1use alloc::vec::Vec;
30
31pub const SY_TEXT: u8 = 0;
33pub const SY_KEYWORD: u8 = 1;
35pub const SY_TYPE: u8 = 2;
37pub const SY_FUNC: u8 = 3;
39pub const SY_STRING: u8 = 4;
41pub const SY_NUMBER: u8 = 5;
43pub const SY_COMMENT: u8 = 6;
45pub const SY_CONST: u8 = 7;
47pub const SY_PUNCT: u8 = 8;
49pub const SY_META: u8 = 9;
51pub const SY_KEY: u8 = 10;
53pub const SY_HEADING: u8 = 11;
55pub const SY_EMPH: u8 = 12;
57pub const SY_STRONG: u8 = 13;
59pub const SY_LINK: u8 = 14;
61pub const SY_CODE: u8 = 15;
63pub const SY_ESCAPE: u8 = 16;
65pub const SY_COUNT: usize = 17;
67
68#[derive(Clone, Copy, Debug, PartialEq, Eq)]
70pub struct Token {
71 pub start: usize,
72 pub end: usize,
73 pub kind: u8,
74}
75
76#[non_exhaustive]
78pub enum Rule {
79 LineComment(&'static str),
81 Block { open: &'static str, close: &'static str, kind: u8, nested: bool, bol: bool },
84 Str { open: &'static str, close: &'static str, escape: bool, multi: bool, kind: u8 },
87 RawStr,
89 Interp { open: &'static str, close: &'static str, start: &'static str },
94 Words(&'static [&'static str], u8),
96 Number,
98 Custom(fn(&str, usize, bool) -> Option<(usize, u8)>),
100 Punct(&'static str),
102}
103
104pub struct Grammar {
106 pub name: &'static str,
107 pub exts: &'static [&'static str],
109 pub rules: &'static [Rule],
110 pub ident: &'static str,
112 pub calls: bool,
114 pub macros: bool,
116 pub caps_types: bool,
118 pub comment: &'static str,
120 pub openers: &'static str,
122 pub closers: &'static str,
124 pub pairs: &'static str,
126}
127
128#[derive(Clone, Copy)]
130pub struct Highlighter {
131 g: &'static Grammar,
132}
133
134fn push(out: &mut Vec<Token>, start: usize, end: usize, kind: u8) {
135 if end <= start {
136 return;
137 }
138 if let Some(t) = out.last_mut() {
139 if t.kind == kind && t.end == start {
140 t.end = end;
141 return;
142 }
143 }
144 out.push(Token { start, end, kind });
145}
146
147fn enc(rule: usize, depth: u32, extra: u32) -> u32 {
148 (rule as u32 + 1) & 0xFF | (depth.min(255) << 8) | (extra.min(0xFFFF) << 16)
149}
150
151fn utf8_len(b: u8) -> usize {
152 match b {
153 0..=0x7F => 1,
154 0xC0..=0xDF => 2,
155 0xE0..=0xEF => 3,
156 0xF0..=0xF7 => 4,
157 _ => 1,
158 }
159}
160
161impl Highlighter {
162 pub const fn new(g: &'static Grammar) -> Highlighter {
164 Highlighter { g }
165 }
166 pub const fn plain() -> Highlighter {
168 Highlighter { g: &PLAIN }
169 }
170 pub const fn rust() -> Highlighter {
172 Highlighter { g: &RUST }
173 }
174 pub const fn json() -> Highlighter {
176 Highlighter { g: &JSON }
177 }
178 pub const fn ini() -> Highlighter {
180 Highlighter { g: &INI }
181 }
182 pub const fn toml() -> Highlighter {
184 Highlighter { g: &TOML }
185 }
186 pub const fn markdown() -> Highlighter {
188 Highlighter { g: &MARKDOWN }
189 }
190 pub const fn jfs() -> Highlighter {
192 Highlighter { g: &JFS }
193 }
194
195 pub fn for_name(s: &str) -> Highlighter {
197 let s = s.trim();
198 let ext = s.rsplit(['/', '\\']).next().unwrap_or(s);
199 let ext = ext.rsplit('.').next().unwrap_or(ext).to_ascii_lowercase();
200 for g in [&RUST, &JSON, &INI, &TOML, &MARKDOWN, &JFS] {
201 if g.name.eq_ignore_ascii_case(s) || g.exts.contains(&ext.as_str()) {
202 return Highlighter { g };
203 }
204 }
205 Highlighter::plain()
206 }
207
208 pub fn grammar(&self) -> &'static Grammar {
210 self.g
211 }
212
213 pub fn name(&self) -> &'static str {
215 self.g.name
216 }
217
218 fn ident_char(&self, c: char) -> bool {
219 c.is_alphanumeric() || c == '_' || self.g.ident.contains(c)
220 }
221
222 fn ident_at(&self, t: &str, i: usize) -> Option<usize> {
224 let c = t[i..].chars().next()?;
225 if !(c.is_alphabetic() || c == '_') {
226 return None;
227 }
228 let mut e = i + c.len_utf8();
229 for ch in t[e..].chars() {
230 if !self.ident_char(ch) {
231 break;
232 }
233 e += ch.len_utf8();
234 }
235 Some(e)
236 }
237
238 fn prev_ident(&self, t: &str, i: usize) -> bool {
239 t[..i].chars().next_back().is_some_and(|c| self.ident_char(c))
240 }
241
242 pub fn line(&self, t: &str, state: u32, out: &mut Vec<Token>) -> u32 {
244 out.clear();
245 self.line_part(t, state, 0, out, &mut |_, _| false).1.unwrap_or(0)
246 }
247
248 pub(crate) fn line_part(&self, t: &str, state: u32, from: usize, out: &mut Vec<Token>, at: &mut dyn FnMut(usize, &[Token]) -> bool) -> (usize, Option<u32>) {
253 let mut i = from;
254 let bol = t.len() - t.trim_start_matches([' ', '\t']).len();
255 if from == 0 && state != 0 {
256 let (e, st) = self.resume(t, state, bol, out);
257 if st != 0 {
258 return (t.len(), Some(st));
259 }
260 i = e;
261 }
262 self.scan(t, i, bol, false, out, at)
263 }
264
265 fn scan(&self, t: &str, mut i: usize, bol: usize, code: bool, out: &mut Vec<Token>, at: &mut dyn FnMut(usize, &[Token]) -> bool) -> (usize, Option<u32>) {
269 let b = t.as_bytes();
270 let n = b.len();
271 let rules = self.g.rules;
272 let mut braces = 0u32;
273 'outer: while i < n {
274 if !code && at(i, out) {
275 return (i, None);
276 }
277 if code && b[i] == b'{' {
278 braces += 1;
279 } else if code && b[i] == b'}' {
280 if braces == 0 {
281 return (i, Some(0));
282 }
283 braces -= 1;
284 }
285 let at_bol = i == bol;
286 let prev = self.prev_ident(t, i);
287 for (ri, rule) in rules.iter().enumerate() {
288 match rule {
289 Rule::LineComment(p) => {
290 if t[i..].starts_with(p) {
291 push(out, i, n, SY_COMMENT);
292 return (n, Some(0));
293 }
294 }
295 Rule::Block { open, close, kind, nested, bol: only } => {
296 if (!*only || at_bol) && t[i..].starts_with(open) && !(prev && open.starts_with(|c: char| c.is_alphanumeric())) {
297 if *only {
298 push(out, i, n, *kind);
299 return (n, Some(enc(ri, 1, 0)));
300 }
301 let (e, depth) = scan_block(t, i + open.len(), open, close, *nested, 1);
302 push(out, i, e, *kind);
303 if depth > 0 {
304 return (n, Some(enc(ri, depth, 0)));
305 }
306 i = e;
307 continue 'outer;
308 }
309 }
310 Rule::Str { open, close, escape, multi, kind } => {
311 if t[i..].starts_with(open) && !(prev && open.starts_with(|c: char| c.is_alphanumeric())) {
312 let (e, done) = scan_str(t, i, i + open.len(), close, *escape, *kind, out);
313 if !done && *multi {
314 return (n, Some(enc(ri, 0, 0)));
315 }
316 i = e;
317 continue 'outer;
318 }
319 }
320 Rule::RawStr => {
321 if let Some((q, hashes)) = raw_open(t, i).filter(|_| !prev) {
322 match raw_close(t, q, hashes) {
323 Some(e) => {
324 push(out, i, e, SY_STRING);
325 i = e;
326 continue 'outer;
327 }
328 None => {
329 push(out, i, n, SY_STRING);
330 return (n, Some(enc(ri, 0, hashes as u32)));
331 }
332 }
333 }
334 }
335 Rule::Interp { open, close, start } => {
336 if t[i..].starts_with(open) && !(prev && open.starts_with(|c: char| c.is_alphanumeric())) {
337 let (e, st) = self.interp(t, i, open.len(), close, start, bol, out);
338 if st != 0 {
339 return (n, Some(st));
340 }
341 i = e;
342 continue 'outer;
343 }
344 }
345 Rule::Words(list, kind) => {
346 if !prev {
347 if let Some(e) = self.ident_at(t, i) {
348 if list.contains(&&t[i..e]) {
349 push(out, i, e, *kind);
350 i = e;
351 continue 'outer;
352 }
353 }
354 }
355 }
356 Rule::Number => {
357 if !prev && b[i].is_ascii_digit() {
358 let e = number_end(t, i);
359 push(out, i, e, SY_NUMBER);
360 i = e;
361 continue 'outer;
362 }
363 }
364 Rule::Custom(f) => {
365 if let Some((len, kind)) = f(t, i, at_bol) {
366 if len > 0 {
367 let e = (i + len).min(n);
368 push(out, i, e, kind);
369 i = e;
370 while i < n && !t.is_char_boundary(i) {
371 i += 1;
372 }
373 continue 'outer;
374 }
375 }
376 }
377 Rule::Punct(chars) => {
378 if b[i] < 0x80 && chars.as_bytes().contains(&b[i]) {
379 push(out, i, i + 1, SY_PUNCT);
380 i += 1;
381 continue 'outer;
382 }
383 }
384 }
385 }
386 if !prev {
388 if let Some(e) = self.ident_at(t, i) {
389 let g = self.g;
390 let mut next = e;
391 while next < n && b[next] == b' ' {
392 next += 1;
393 }
394 let kind = if g.macros && b.get(e) == Some(&b'!') && b.get(e + 1) != Some(&b'=') {
395 SY_FUNC
396 } else if g.calls && b.get(next) == Some(&b'(') {
397 SY_FUNC
398 } else if g.caps_types && t[i..].chars().next().is_some_and(|c| c.is_uppercase()) {
399 SY_TYPE
400 } else {
401 SY_TEXT
402 };
403 if kind != SY_TEXT {
404 push(out, i, if kind == SY_FUNC && g.macros && b.get(e) == Some(&b'!') { e + 1 } else { e }, kind);
405 if kind == SY_FUNC && g.macros && b.get(e) == Some(&b'!') {
406 i = e + 1;
407 continue;
408 }
409 }
410 i = e;
411 continue;
412 }
413 }
414 i += utf8_len(b[i]);
415 }
416 (n, Some(0))
417 }
418
419 #[allow(clippy::too_many_arguments)]
422 fn interp(&self, t: &str, i: usize, open: usize, close: &str, start: &str, bol: usize, out: &mut Vec<Token>) -> (usize, u32) {
423 let b = t.as_bytes();
424 let mut s = i;
425 let mut j = i + open;
426 while j < b.len() {
427 if b[j] == b'\\' {
428 push(out, s, j, SY_STRING);
429 let mut e = (j + 1 + b.get(j + 1).map_or(0, |&c| utf8_len(c))).min(b.len());
430 if b.get(j + 1) == Some(&b'u') && b.get(j + 2) == Some(&b'{') {
431 e = j + 3 + b[j + 3..].iter().take_while(|c| c.is_ascii_hexdigit()).count();
432 if b.get(e) == Some(&b'}') {
433 e += 1;
434 }
435 }
436 while e < b.len() && !t.is_char_boundary(e) {
437 e += 1;
438 }
439 push(out, j, e, SY_ESCAPE);
440 j = e;
441 s = e;
442 continue;
443 }
444 if t[j..].starts_with(close) {
445 push(out, s, j + close.len(), SY_STRING);
446 return (j + close.len(), 0);
447 }
448 if t[j..].starts_with(start) {
449 push(out, s, j, SY_STRING);
450 push(out, j, j + start.len(), SY_META);
451 let (e, st) = self.scan(t, j + start.len(), bol, true, out, &mut |_, _| false);
452 let st = st.unwrap_or(0);
453 if st != 0 || e >= b.len() {
454 return (b.len(), st);
455 }
456 push(out, e, e + 1, SY_META);
457 j = e + 1;
458 s = j;
459 continue;
460 }
461 j += utf8_len(b[j]);
462 }
463 push(out, s, b.len(), SY_STRING);
464 (b.len(), 0)
465 }
466
467 fn resume(&self, t: &str, state: u32, bol: usize, out: &mut Vec<Token>) -> (usize, u32) {
469 let n = t.len();
470 let ri = (state & 0xFF) as usize - 1;
471 let Some(rule) = self.g.rules.get(ri) else { return (0, 0) };
472 match rule {
473 Rule::Block { open, close, kind, nested, bol: only } => {
474 if *only {
475 push(out, 0, n, *kind);
476 return if t[bol..].starts_with(close) { (n, 0) } else { (n, state) };
477 }
478 let (e, depth) = scan_block(t, 0, open, close, *nested, (state >> 8) & 0xFF);
479 push(out, 0, e, *kind);
480 if depth > 0 { (n, enc(ri, depth, 0)) } else { (e, 0) }
481 }
482 Rule::Str { close, escape, kind, .. } => {
483 let (e, done) = scan_str(t, 0, 0, close, *escape, *kind, out);
484 if done { (e, 0) } else { (n, state) }
485 }
486 Rule::RawStr => {
487 let hashes = (state >> 16) as usize;
488 match raw_close(t, 0, hashes) {
489 Some(e) => {
490 push(out, 0, e, SY_STRING);
491 (e, 0)
492 }
493 None => {
494 push(out, 0, n, SY_STRING);
495 (n, state)
496 }
497 }
498 }
499 _ => (0, 0),
500 }
501 }
502
503 pub fn tokens(&self, text: &str) -> Vec<Vec<Token>> {
505 let mut st = 0;
506 let mut out = Vec::new();
507 for l in crate::rich_doc::lines_of(text) {
508 let mut v = Vec::new();
509 st = self.line(l, st, &mut v);
510 out.push(v);
511 }
512 out
513 }
514}
515
516fn scan_block(t: &str, i: usize, open: &str, close: &str, nested: bool, depth: u32) -> (usize, u32) {
518 let mut d = depth;
519 let mut j = i;
520 while j < t.len() {
521 if t[j..].starts_with(close) {
522 d -= 1;
523 j += close.len();
524 if d == 0 {
525 return (j, 0);
526 }
527 continue;
528 }
529 if nested && t[j..].starts_with(open) {
530 d += 1;
531 j += open.len();
532 continue;
533 }
534 j += utf8_len(t.as_bytes()[j]);
535 }
536 (t.len(), d.max(1))
537}
538
539fn scan_str(t: &str, start: usize, i: usize, close: &str, escape: bool, kind: u8, out: &mut Vec<Token>) -> (usize, bool) {
541 let b = t.as_bytes();
542 let mut s = start;
543 let mut j = i;
544 while j < b.len() {
545 if escape && b[j] == b'\\' {
546 push(out, s, j, kind);
547 let mut e = (j + 1 + b.get(j + 1).map_or(0, |&c| utf8_len(c))).min(b.len());
548 if b.get(j + 1) == Some(&b'u') && b.get(j + 2) == Some(&b'{') {
549 e = t[j..].find('}').map_or(b.len(), |k| j + k + 1);
550 } else if b.get(j + 1) == Some(&b'x') {
551 e = (j + 4).min(b.len());
552 } else if b.get(j + 1) == Some(&b'u') && kind == SY_STRING {
553 e = (j + 6).min(b.len());
554 }
555 while e < b.len() && !t.is_char_boundary(e) {
556 e += 1;
557 }
558 push(out, j, e, SY_ESCAPE);
559 j = e;
560 s = e;
561 continue;
562 }
563 if t[j..].starts_with(close) {
564 let e = j + close.len();
565 push(out, s, e, kind);
566 return (e, true);
567 }
568 j += utf8_len(b[j]);
569 }
570 push(out, s, b.len(), kind);
571 (b.len(), false)
572}
573
574fn raw_open(t: &str, i: usize) -> Option<(usize, usize)> {
576 let b = t.as_bytes();
577 let mut j = i;
578 if b.get(j) == Some(&b'b') || b.get(j) == Some(&b'c') {
579 j += 1;
580 }
581 if b.get(j) != Some(&b'r') {
582 return None;
583 }
584 j += 1;
585 let mut h = 0;
586 while b.get(j) == Some(&b'#') {
587 h += 1;
588 j += 1;
589 }
590 (b.get(j) == Some(&b'"')).then_some((j + 1, h))
591}
592
593fn raw_close(t: &str, i: usize, hashes: usize) -> Option<usize> {
595 let b = t.as_bytes();
596 let mut j = i;
597 while j < b.len() {
598 if b[j] == b'"' && b[j + 1..].iter().take(hashes).filter(|&&c| c == b'#').count() == hashes && b.len() >= j + 1 + hashes {
599 return Some(j + 1 + hashes);
600 }
601 j += 1;
602 }
603 None
604}
605
606fn number_end(t: &str, i: usize) -> usize {
608 let b = t.as_bytes();
609 let mut j = i;
610 let hex = b[i] == b'0' && matches!(b.get(i + 1), Some(b'x') | Some(b'X'));
611 if hex || (b[i] == b'0' && matches!(b.get(i + 1), Some(b'b') | Some(b'o') | Some(b'B') | Some(b'O'))) {
612 j += 2;
613 }
614 while j < b.len() {
615 let c = b[j];
616 if c.is_ascii_alphanumeric() || c == b'_' {
617 if !hex && (c == b'e' || c == b'E') && matches!(b.get(j + 1), Some(b'+') | Some(b'-')) && b.get(j + 2).is_some_and(|d| d.is_ascii_digit()) {
619 j += 2;
620 continue;
621 }
622 j += 1;
623 } else if c == b'.' && b.get(j + 1).is_some_and(|d| d.is_ascii_digit()) && !t[i..j].contains('.') {
624 j += 1;
625 } else {
626 break;
627 }
628 }
629 j
630}
631
632fn rust_quote(t: &str, i: usize, _: bool) -> Option<(usize, u8)> {
636 let b = t.as_bytes();
637 if b[i] != b'\'' {
638 return None;
639 }
640 if b.get(i + 1) == Some(&b'\\') {
641 let e = t[i + 2..].find('\'').map_or(t.len(), |k| i + 2 + k + 1);
642 return Some((e - i, SY_STRING));
643 }
644 let c = t[i + 1..].chars().next()?;
645 let after = i + 1 + c.len_utf8();
646 if b.get(after) == Some(&b'\'') {
647 return Some((after + 1 - i, SY_STRING));
648 }
649 if c.is_alphabetic() || c == '_' {
650 let mut e = after;
651 for ch in t[after..].chars() {
652 if !(ch.is_alphanumeric() || ch == '_') {
653 break;
654 }
655 e += ch.len_utf8();
656 }
657 return Some((e - i, SY_META));
658 }
659 None
660}
661
662fn rust_attr(t: &str, i: usize, _: bool) -> Option<(usize, u8)> {
664 let r = &t[i..];
665 let start = if r.starts_with("#[") {
666 2
667 } else if r.starts_with("#![") {
668 3
669 } else {
670 return None;
671 };
672 let mut d = 1;
673 let mut in_str = false;
674 for (k, c) in r[start..].char_indices() {
675 match c {
676 '"' => in_str = !in_str,
677 '[' if !in_str => d += 1,
678 ']' if !in_str => {
679 d -= 1;
680 if d == 0 {
681 return Some((start + k + 1, SY_META));
682 }
683 }
684 _ => {}
685 }
686 }
687 Some((r.len(), SY_META))
688}
689
690fn json_string(t: &str, i: usize, _: bool) -> Option<(usize, u8)> {
692 let b = t.as_bytes();
693 if b[i] != b'"' {
694 return None;
695 }
696 let mut j = i + 1;
697 while j < b.len() {
698 match b[j] {
699 b'\\' => j += 2,
700 b'"' => {
701 j += 1;
702 let rest = t[j.min(t.len())..].trim_start();
703 return Some((j - i, if rest.starts_with(':') { SY_KEY } else { SY_STRING }));
704 }
705 _ => j += 1,
706 }
707 }
708 Some((b.len() - i, SY_STRING))
709}
710
711fn ini_section(t: &str, i: usize, bol: bool) -> Option<(usize, u8)> {
713 if !bol || t.as_bytes()[i] != b'[' {
714 return None;
715 }
716 let e = t[i..].find(']').map_or(t.len(), |k| {
717 let mut e = i + k + 1;
718 if t[e..].starts_with(']') {
719 e += 1;
720 }
721 e
722 });
723 Some((e - i, SY_HEADING))
724}
725
726fn ini_key(t: &str, i: usize, bol: bool) -> Option<(usize, u8)> {
728 if !bol {
729 return None;
730 }
731 let r = &t[i..];
732 let c = r.chars().next()?;
733 if matches!(c, '[' | ';' | '#' | '=') {
734 return None;
735 }
736 let sep = r.find(['=', ':'])?;
737 let key = r[..sep].trim_end();
738 if key.is_empty() || key.contains(['"', '\'']) && !key.starts_with(['"', '\'']) {
739 return None;
740 }
741 Some((key.len(), SY_KEY))
742}
743
744fn url_len(r: &str) -> Option<usize> {
746 if !(r.starts_with("http://") || r.starts_with("https://")) {
747 return None;
748 }
749 let e = r.find(|c: char| c.is_whitespace() || matches!(c, ')' | '>' | '"' | '\'' | ']')).unwrap_or(r.len());
750 Some(e)
751}
752
753fn md_heading(t: &str, i: usize, bol: bool) -> Option<(usize, u8)> {
755 if !bol {
756 return None;
757 }
758 let r = &t[i..];
759 let h = r.bytes().take_while(|&c| c == b'#').count();
760 if h == 0 || h > 6 || !(r.len() == h || r.as_bytes()[h] == b' ') {
761 return None;
762 }
763 Some((r.len(), SY_HEADING))
764}
765
766fn md_quote(t: &str, i: usize, bol: bool) -> Option<(usize, u8)> {
768 (bol && t.as_bytes()[i] == b'>').then_some((t.len() - i, SY_COMMENT))
769}
770
771fn md_rule(t: &str, i: usize, bol: bool) -> Option<(usize, u8)> {
773 if !bol {
774 return None;
775 }
776 let r = t[i..].trim_end();
777 let c = r.bytes().next()?;
778 if !matches!(c, b'-' | b'*' | b'_') {
779 return None;
780 }
781 let n = r.bytes().filter(|&x| x == c).count();
782 (n >= 3 && r.bytes().all(|x| x == c || x == b' ')).then_some((t.len() - i, SY_PUNCT))
783}
784
785fn md_list(t: &str, i: usize, bol: bool) -> Option<(usize, u8)> {
787 if !bol {
788 return None;
789 }
790 let b = &t.as_bytes()[i..];
791 if b.len() >= 2 && matches!(b[0], b'-' | b'*' | b'+') && b[1] == b' ' {
792 return Some((1, SY_KEYWORD));
793 }
794 let d = b.iter().take_while(|c| c.is_ascii_digit()).count();
795 if d > 0 && d < 10 && b.len() > d + 1 && matches!(b[d], b'.' | b')') && b[d + 1] == b' ' {
796 return Some((d + 1, SY_KEYWORD));
797 }
798 None
799}
800
801fn md_emph(t: &str, i: usize, _: bool) -> Option<(usize, u8)> {
803 let b = t.as_bytes();
804 let c = b[i];
805 if c != b'*' && c != b'_' {
806 return None;
807 }
808 if c == b'_' && t[..i].chars().next_back().is_some_and(|p| p.is_alphanumeric()) {
809 return None;
810 }
811 let strong = b.get(i + 1) == Some(&c);
812 let delim = if strong { 2 } else { 1 };
813 let inner = i + delim;
814 if b.get(inner).is_none_or(|x| *x == b' ' || *x == c) {
815 return None;
816 }
817 let pat: &[u8] = if strong { &b[i..i + 2] } else { &b[i..i + 1] };
818 let mut j = inner + 1;
819 while j + delim <= b.len() {
820 if &b[j..j + delim] == pat && b[j - 1] != b' ' && (strong || b.get(j + 1) != Some(&c)) {
821 if c == b'_' && t[j + delim..].chars().next().is_some_and(|p| p.is_alphanumeric()) {
822 j += 1;
823 continue;
824 }
825 return Some((j + delim - i, if strong { SY_STRONG } else { SY_EMPH }));
826 }
827 j += 1;
828 }
829 None
830}
831
832fn md_link(t: &str, i: usize, _: bool) -> Option<(usize, u8)> {
834 let r = &t[i..];
835 if let Some(e) = url_len(r) {
836 return Some((e, SY_LINK));
837 }
838 if r.starts_with('<') {
839 let e = r.find('>')?;
840 return url_len(&r[1..]).filter(|&u| u + 1 == e).map(|_| (e + 1, SY_LINK));
841 }
842 let s = if r.starts_with("![") { 1 } else if r.starts_with('[') { 0 } else { return None };
843 let close = r[s..].find("](")? + s;
844 let end = r[close + 2..].find(')')? + close + 3;
845 Some((end, SY_LINK))
846}
847
848fn md_escape(t: &str, i: usize, _: bool) -> Option<(usize, u8)> {
850 let b = t.as_bytes();
851 (b[i] == b'\\' && b.get(i + 1).is_some_and(|c| c.is_ascii_punctuation())).then_some((2, SY_ESCAPE))
852}
853
854fn toml_date(t: &str, i: usize, _: bool) -> Option<(usize, u8)> {
856 let b = &t.as_bytes()[i..];
857 if b.len() < 5 || !b[0].is_ascii_digit() {
858 return None;
859 }
860 let date = b.len() >= 10 && b[4] == b'-' && b[7] == b'-';
861 let time = b.len() >= 8 && b[2] == b':' && b[5] == b':';
862 if !date && !time {
863 return None;
864 }
865 let e = b.iter().position(|c| !(c.is_ascii_alphanumeric() || matches!(c, b'-' | b':' | b'.' | b'+'))).unwrap_or(b.len());
866 Some((e, SY_NUMBER))
867}
868
869const RUST_KEYWORDS: &[&str] = &[
872 "as", "async", "await", "break", "const", "continue", "crate", "dyn", "else", "enum", "extern", "fn", "for", "if", "impl", "in", "let", "loop",
873 "match", "mod", "move", "mut", "pub", "ref", "return", "self", "static", "struct", "super", "trait", "type", "union", "unsafe", "use", "where",
874 "while", "yield", "macro_rules", "try", "gen",
875];
876const RUST_TYPES: &[&str] = &[
877 "u8", "u16", "u32", "u64", "u128", "usize", "i8", "i16", "i32", "i64", "i128", "isize", "f32", "f64", "bool", "char", "str", "Self",
878];
879const RUST_CONSTS: &[&str] = &["true", "false", "None", "Some", "Ok", "Err"];
880
881static RUST_RULES: [Rule; 12] = [
882 Rule::LineComment("//"),
883 Rule::Block { open: "/*", close: "*/", kind: SY_COMMENT, nested: true, bol: false },
884 Rule::RawStr,
885 Rule::Str { open: "b\"", close: "\"", escape: true, multi: true, kind: SY_STRING },
886 Rule::Str { open: "\"", close: "\"", escape: true, multi: true, kind: SY_STRING },
887 Rule::Custom(rust_quote),
888 Rule::Custom(rust_attr),
889 Rule::Words(RUST_KEYWORDS, SY_KEYWORD),
890 Rule::Words(RUST_TYPES, SY_TYPE),
891 Rule::Words(RUST_CONSTS, SY_CONST),
892 Rule::Number,
893 Rule::Punct("+-*/%=<>!&|^~?:;,.()[]{}@#$"),
894];
895
896pub static RUST: Grammar = Grammar {
898 name: "Rust",
899 exts: &["rs"],
900 rules: &RUST_RULES,
901 ident: "",
902 calls: true,
903 macros: true,
904 caps_types: true,
905 comment: "//",
906 openers: "{[(",
907 closers: "}])",
908 pairs: "()[]{}",
909};
910
911static JSON_RULES: [Rule; 6] = [
912 Rule::LineComment("//"),
913 Rule::Block { open: "/*", close: "*/", kind: SY_COMMENT, nested: false, bol: false },
914 Rule::Custom(json_string),
915 Rule::Words(&["true", "false", "null"], SY_CONST),
916 Rule::Number,
917 Rule::Punct("{}[],:-"),
918];
919
920pub static JSON: Grammar = Grammar {
922 name: "JSON",
923 exts: &["json", "jsonc", "geojson"],
924 rules: &JSON_RULES,
925 ident: "",
926 calls: false,
927 macros: false,
928 caps_types: false,
929 comment: "//",
930 openers: "{[",
931 closers: "}]",
932 pairs: "[]{}",
933};
934
935const INI_CONSTS: &[&str] = &["true", "false", "yes", "no", "on", "off", "TRUE", "FALSE", "Yes", "No", "On", "Off", "True", "False"];
936
937fn ini_comment(t: &str, i: usize, bol: bool) -> Option<(usize, u8)> {
938 (bol && matches!(t.as_bytes()[i], b';' | b'#')).then_some((t.len() - i, SY_COMMENT))
939}
940
941static INI_RULES: [Rule; 8] = [
942 Rule::Custom(ini_comment),
943 Rule::Custom(ini_section),
944 Rule::Custom(ini_key),
945 Rule::Str { open: "\"", close: "\"", escape: true, multi: false, kind: SY_STRING },
946 Rule::Str { open: "'", close: "'", escape: false, multi: false, kind: SY_STRING },
947 Rule::Words(INI_CONSTS, SY_CONST),
948 Rule::Number,
949 Rule::Punct("=:,[]"),
950];
951
952pub static INI: Grammar = Grammar {
954 name: "INI",
955 exts: &["ini", "cfg", "conf", "desktop", "properties", "inf", "reg"],
956 rules: &INI_RULES,
957 ident: "-.",
958 calls: false,
959 macros: false,
960 caps_types: false,
961 comment: ";",
962 openers: "",
963 closers: "",
964 pairs: "[]",
965};
966
967static TOML_RULES: [Rule; 12] = [
968 Rule::LineComment("#"),
969 Rule::Custom(ini_section),
970 Rule::Custom(ini_key),
971 Rule::Str { open: "\"\"\"", close: "\"\"\"", escape: true, multi: true, kind: SY_STRING },
972 Rule::Str { open: "'''", close: "'''", escape: false, multi: true, kind: SY_STRING },
973 Rule::Str { open: "\"", close: "\"", escape: true, multi: false, kind: SY_STRING },
974 Rule::Str { open: "'", close: "'", escape: false, multi: false, kind: SY_STRING },
975 Rule::Words(&["true", "false", "inf", "nan"], SY_CONST),
976 Rule::Custom(toml_date),
977 Rule::Number,
978 Rule::Punct("=,.[]{}+-"),
979 Rule::Punct(":"),
980];
981
982pub static TOML: Grammar = Grammar {
984 name: "TOML",
985 exts: &["toml", "lock"],
986 rules: &TOML_RULES,
987 ident: "-",
988 calls: false,
989 macros: false,
990 caps_types: false,
991 comment: "#",
992 openers: "[{",
993 closers: "]}",
994 pairs: "[]{}",
995};
996
997static MD_RULES: [Rule; 11] = [
998 Rule::Block { open: "```", close: "```", kind: SY_CODE, nested: false, bol: true },
999 Rule::Block { open: "~~~", close: "~~~", kind: SY_CODE, nested: false, bol: true },
1000 Rule::Block { open: "<!--", close: "-->", kind: SY_COMMENT, nested: false, bol: false },
1001 Rule::Custom(md_heading),
1002 Rule::Custom(md_quote),
1003 Rule::Custom(md_rule),
1004 Rule::Custom(md_list),
1005 Rule::Custom(md_escape),
1006 Rule::Str { open: "`", close: "`", escape: false, multi: false, kind: SY_CODE },
1007 Rule::Custom(md_emph),
1008 Rule::Custom(md_link),
1009];
1010
1011pub static MARKDOWN: Grammar = Grammar {
1014 name: "Markdown",
1015 exts: &["md", "markdown", "mdown"],
1016 rules: &MD_RULES,
1017 ident: "",
1018 calls: false,
1019 macros: false,
1020 caps_types: false,
1021 comment: "",
1022 openers: "",
1023 closers: "",
1024 pairs: "()[]",
1025};
1026
1027const JFS_KEYWORDS: &[&str] = &["let", "fn", "if", "else", "while", "for", "in", "break", "continue", "return", "try", "catch", "throw"];
1031
1032static JFS_RULES: [Rule; 7] = [
1033 Rule::LineComment("//"),
1034 Rule::Block { open: "/*", close: "*/", kind: SY_COMMENT, nested: false, bol: false },
1035 Rule::Interp { open: "\"", close: "\"", start: "${" },
1036 Rule::Words(JFS_KEYWORDS, SY_KEYWORD),
1037 Rule::Words(&["true", "false", "nil"], SY_CONST),
1038 Rule::Number,
1039 Rule::Punct("+-*/%=<>!&|?:;,.()[]{}"),
1040];
1041
1042pub static JFS: Grammar = Grammar {
1045 name: "JFS",
1046 exts: &["jfs"],
1047 rules: &JFS_RULES,
1048 ident: "",
1049 calls: true,
1050 macros: false,
1051 caps_types: false,
1052 comment: "//",
1053 openers: "{[(",
1054 closers: "}])",
1055 pairs: "()[]{}",
1056};
1057
1058static PLAIN_RULES: [Rule; 0] = [];
1059
1060pub static PLAIN: Grammar = Grammar {
1062 name: "Text",
1063 exts: &["txt", "text", "log"],
1064 rules: &PLAIN_RULES,
1065 ident: "",
1066 calls: false,
1067 macros: false,
1068 caps_types: false,
1069 comment: "",
1070 openers: "",
1071 closers: "",
1072 pairs: "()[]{}",
1073};
1074
1075#[derive(Clone, Debug, PartialEq)]
1079pub struct CodeTheme {
1080 pub bg: u32,
1081 pub fg: u32,
1082 pub gutter_bg: u32,
1083 pub gutter_fg: u32,
1084 pub gutter_cur: u32,
1086 pub line_hl: u32,
1088 pub sel: u32,
1090 pub sel_inactive: u32,
1091 pub sel_text: u32,
1093 pub bracket: u32,
1095 pub find: u32,
1097 pub find_cur: u32,
1098 pub find_frame: bool,
1100 pub caret: u32,
1101 pub tokens: [u32; SY_COUNT],
1102 pub bold: u32,
1103 pub italic: u32,
1104 pub diag: [u32; 3],
1106 pub breakpoint: u32,
1108 pub exec_line: u32,
1111 pub exec_mark: u32,
1112}
1113
1114const fn argb(rgb: u32) -> u32 {
1115 0xFF00_0000 | rgb
1116}
1117
1118impl CodeTheme {
1119 pub fn light() -> CodeTheme {
1121 let fg = argb(0x1F2328);
1122 CodeTheme {
1123 bg: argb(0xFFFFFF),
1124 fg,
1125 gutter_bg: argb(0xF6F8FA),
1126 gutter_fg: argb(0x8C959F),
1127 gutter_cur: fg,
1128 line_hl: argb(0xF1F4F8),
1129 sel: argb(0xB6D7FF),
1130 sel_inactive: argb(0xE1E6EB),
1131 sel_text: 0,
1132 bracket: argb(0x8C959F),
1133 find: argb(0xFFE58A),
1134 find_cur: argb(0xFFA94D),
1135 find_frame: false,
1136 caret: fg,
1137 tokens: [
1138 fg,
1139 argb(0xCF222E),
1140 argb(0x953800),
1141 argb(0x8250DF),
1142 argb(0x0A3069),
1143 argb(0x0550AE),
1144 argb(0x6E7781),
1145 argb(0x0550AE),
1146 argb(0x57606A),
1147 argb(0x116329),
1148 argb(0x0550AE),
1149 argb(0x0550AE),
1150 fg,
1151 fg,
1152 argb(0x0969DA),
1153 argb(0x0A3069),
1154 argb(0x116329),
1155 ],
1156 bold: 1 << SY_HEADING | 1 << SY_STRONG,
1157 italic: 1 << SY_COMMENT | 1 << SY_EMPH,
1158 diag: [argb(0xCF222E), argb(0x9A6700), argb(0x0969DA)],
1159 breakpoint: argb(0xD1242F),
1160 exec_line: argb(0xFFF1BF),
1161 exec_mark: argb(0x9A6700),
1162 }
1163 }
1164
1165 pub fn dark() -> CodeTheme {
1167 let fg = argb(0xE6EDF3);
1168 CodeTheme {
1169 bg: argb(0x0D1117),
1170 fg,
1171 gutter_bg: argb(0x0D1117),
1172 gutter_fg: argb(0x6E7681),
1173 gutter_cur: fg,
1174 line_hl: argb(0x161B22),
1175 sel: argb(0x264F78),
1176 sel_inactive: argb(0x30363D),
1177 sel_text: 0,
1178 bracket: argb(0x8B949E),
1179 find: argb(0x5C4A14),
1180 find_cur: argb(0x9E6A03),
1181 find_frame: false,
1182 caret: fg,
1183 tokens: [
1184 fg,
1185 argb(0xFF7B72),
1186 argb(0xFFA657),
1187 argb(0xD2A8FF),
1188 argb(0xA5D6FF),
1189 argb(0x79C0FF),
1190 argb(0x8B949E),
1191 argb(0x79C0FF),
1192 argb(0xC9D1D9),
1193 argb(0x7EE787),
1194 argb(0x79C0FF),
1195 argb(0x79C0FF),
1196 fg,
1197 fg,
1198 argb(0x58A6FF),
1199 argb(0xA5D6FF),
1200 argb(0x7EE787),
1201 ],
1202 bold: 1 << SY_HEADING | 1 << SY_STRONG,
1203 italic: 1 << SY_COMMENT | 1 << SY_EMPH,
1204 diag: [argb(0xF85149), argb(0xD29922), argb(0x58A6FF)],
1205 breakpoint: argb(0xF85149),
1206 exec_line: argb(0x3B3520),
1207 exec_mark: argb(0xE3B341),
1208 }
1209 }
1210
1211 pub fn high_contrast(c: &plat::HcColors) -> CodeTheme {
1215 let (bg, fg, hi, link, dis) = (argb(c.bg), argb(c.text), argb(c.highlight), argb(c.link), argb(c.disabled));
1216 CodeTheme {
1217 bg,
1218 fg,
1219 gutter_bg: bg,
1220 gutter_fg: fg,
1221 gutter_cur: link,
1222 line_hl: bg,
1223 sel: hi,
1224 sel_inactive: dis,
1225 sel_text: argb(c.highlight_text),
1226 bracket: link,
1227 find: link,
1228 find_cur: hi,
1229 find_frame: true,
1230 caret: fg,
1231 tokens: [fg, link, link, fg, hi, hi, dis, hi, fg, link, link, link, fg, fg, link, hi, link],
1232 bold: 1 << SY_KEYWORD | 1 << SY_HEADING | 1 << SY_STRONG | 1 << SY_FUNC,
1233 italic: 1 << SY_COMMENT | 1 << SY_EMPH,
1234 diag: [fg, link, hi],
1235 breakpoint: link,
1236 exec_line: bg,
1237 exec_mark: hi,
1238 }
1239 }
1240}
1241
1242impl crate::Ui {
1243 pub fn code_theme(&self) -> CodeTheme {
1247 use crate::consts::*;
1248 use crate::theme::color_mix;
1249 if self.high_contrast {
1250 let c = self.hc_sys.unwrap_or(if self.theme_dark { plat::HcColors::BLACK } else { plat::HcColors::WHITE });
1251 return CodeTheme::high_contrast(&c);
1252 }
1253 let mut t = if self.theme_dark { CodeTheme::dark() } else { CodeTheme::light() };
1254 let over = |bg: u32, c: u32| -> u32 {
1255 let a = (c >> 24) as i32;
1256 color_mix(bg | 0xFF00_0000, c | 0xFF00_0000, a + (a >> 7))
1257 };
1258 let bg = over(self.color(T_BG), self.color(T_INPUT_ACTIVE));
1259 let fg = over(bg, self.color(T_TEXT));
1260 t.bg = bg;
1261 t.fg = fg;
1262 t.tokens[SY_TEXT as usize] = fg;
1263 t.tokens[SY_EMPH as usize] = fg;
1264 t.tokens[SY_STRONG as usize] = fg;
1265 t.caret = fg;
1266 t.gutter_bg = color_mix(bg, fg, 10);
1267 t.gutter_fg = over(t.gutter_bg, self.color(T_TEXT3));
1268 t.gutter_cur = fg;
1269 t.line_hl = color_mix(bg, fg, if self.theme_dark { 16 } else { 10 });
1270 t.sel = color_mix(bg, self.color(T_ACCENT) | 0xFF00_0000, if self.theme_dark { 110 } else { 80 });
1271 t.sel_inactive = color_mix(bg, fg, if self.theme_dark { 40 } else { 30 });
1272 t.diag = [over(bg, self.color(T_CRIT)), over(bg, self.color(T_WARN)), over(bg, self.color(T_INFO))];
1273 t.breakpoint = t.diag[0];
1274 t.exec_mark = t.diag[1];
1275 t.exec_line = color_mix(bg, t.diag[1], if self.theme_dark { 48 } else { 40 });
1276 t
1277 }
1278}