1use alloc::vec::Vec;
37
38pub const SY_TEXT: u8 = 0;
40pub const SY_KEYWORD: u8 = 1;
42pub const SY_TYPE: u8 = 2;
44pub const SY_FUNC: u8 = 3;
46pub const SY_STRING: u8 = 4;
48pub const SY_NUMBER: u8 = 5;
50pub const SY_COMMENT: u8 = 6;
52pub const SY_CONST: u8 = 7;
54pub const SY_PUNCT: u8 = 8;
56pub const SY_META: u8 = 9;
58pub const SY_KEY: u8 = 10;
60pub const SY_HEADING: u8 = 11;
62pub const SY_EMPH: u8 = 12;
64pub const SY_STRONG: u8 = 13;
66pub const SY_LINK: u8 = 14;
68pub const SY_CODE: u8 = 15;
70pub const SY_ESCAPE: u8 = 16;
72pub const SY_COUNT: usize = 17;
74
75#[derive(Clone, Copy, Debug, PartialEq, Eq)]
77pub struct Token {
78 pub start: usize,
79 pub end: usize,
80 pub kind: u8,
81}
82
83#[non_exhaustive]
85pub enum Rule {
86 LineComment(&'static str),
88 Block { open: &'static str, close: &'static str, kind: u8, nested: bool, bol: bool },
91 Str { open: &'static str, close: &'static str, escape: bool, multi: bool, kind: u8 },
94 RawStr,
96 Interp { open: &'static str, close: &'static str, start: &'static str },
101 Words(&'static [&'static str], u8),
103 Number,
105 Custom(fn(&str, usize, bool) -> Option<(usize, u8)>),
107 Punct(&'static str),
109 Nest { open: &'static str, close: &'static str, kind: u8, inner: &'static Grammar },
114 Element { name: &'static str, tag: &'static Grammar, inner: &'static Grammar },
120}
121
122pub struct Grammar {
124 pub name: &'static str,
125 pub exts: &'static [&'static str],
127 pub rules: &'static [Rule],
128 pub ident: &'static str,
130 pub calls: bool,
132 pub macros: bool,
134 pub caps_types: bool,
136 pub comment: &'static str,
138 pub openers: &'static str,
140 pub closers: &'static str,
142 pub pairs: &'static str,
144}
145
146#[derive(Clone, Copy)]
148pub struct Highlighter {
149 g: &'static Grammar,
150}
151
152fn push(out: &mut Vec<Token>, start: usize, end: usize, kind: u8) {
153 if end <= start {
154 return;
155 }
156 if let Some(t) = out.last_mut() {
157 if t.kind == kind && t.end == start {
158 t.end = end;
159 return;
160 }
161 }
162 out.push(Token { start, end, kind });
163}
164
165fn enc(rule: usize, depth: u32, extra: u32) -> u32 {
166 (rule as u32 + 1) & 0xFF | (depth.min(255) << 8) | (extra.min(0xFFFF) << 16)
167}
168
169fn enc_in(rule: usize, inner: u32) -> u32 {
172 (rule as u32 + 1) & 0xFF | inner << 8
173}
174
175fn find_end_tag(t: &str, i: usize, name: &str) -> Option<usize> {
177 let b = t.as_bytes();
178 let n = name.as_bytes();
179 let mut j = i;
180 while j + 2 + n.len() <= b.len() {
181 if b[j] == b'<' && b[j + 1] == b'/' && b[j + 2..j + 2 + n.len()].eq_ignore_ascii_case(n) {
182 return Some(j);
183 }
184 j += 1;
185 }
186 None
187}
188
189fn element_start(t: &str, i: usize, name: &str) -> Option<usize> {
191 let b = t.as_bytes();
192 let n = name.as_bytes();
193 if b[i] != b'<' || b.len() < i + 1 + n.len() || !b[i + 1..i + 1 + n.len()].eq_ignore_ascii_case(n) {
194 return None;
195 }
196 let e = i + 1 + n.len();
197 if !matches!(b.get(e), Some(b' ' | b'\t' | b'/' | b'>')) {
198 return None;
199 }
200 t[e..].find('>').map(|k| e + k)
201}
202
203fn utf8_len(b: u8) -> usize {
204 match b {
205 0..=0x7F => 1,
206 0xC0..=0xDF => 2,
207 0xE0..=0xEF => 3,
208 0xF0..=0xF7 => 4,
209 _ => 1,
210 }
211}
212
213impl Highlighter {
214 pub const fn new(g: &'static Grammar) -> Highlighter {
216 Highlighter { g }
217 }
218 pub const fn plain() -> Highlighter {
220 Highlighter { g: &PLAIN }
221 }
222 pub const fn rust() -> Highlighter {
224 Highlighter { g: &RUST }
225 }
226 pub const fn json() -> Highlighter {
228 Highlighter { g: &JSON }
229 }
230 pub const fn ini() -> Highlighter {
232 Highlighter { g: &INI }
233 }
234 pub const fn toml() -> Highlighter {
236 Highlighter { g: &TOML }
237 }
238 pub const fn markdown() -> Highlighter {
240 Highlighter { g: &MARKDOWN }
241 }
242 pub const fn jfs() -> Highlighter {
244 Highlighter { g: &JFS }
245 }
246 pub const fn javascript() -> Highlighter {
248 Highlighter { g: &JAVASCRIPT }
249 }
250 pub const fn css() -> Highlighter {
252 Highlighter { g: &CSS }
253 }
254 pub const fn html() -> Highlighter {
257 Highlighter { g: &HTML }
258 }
259 pub const fn wat() -> Highlighter {
261 Highlighter { g: &WAT }
262 }
263
264 pub fn for_name(s: &str) -> Highlighter {
266 let s = s.trim();
267 let ext = s.rsplit(['/', '\\']).next().unwrap_or(s);
268 let ext = ext.rsplit('.').next().unwrap_or(ext).to_ascii_lowercase();
269 for g in [&RUST, &JSON, &INI, &TOML, &MARKDOWN, &JFS, &JAVASCRIPT, &CSS, &HTML, &WAT] {
270 if g.name.eq_ignore_ascii_case(s) || g.exts.contains(&ext.as_str()) {
271 return Highlighter { g };
272 }
273 }
274 Highlighter::plain()
275 }
276
277 pub fn grammar(&self) -> &'static Grammar {
279 self.g
280 }
281
282 pub fn name(&self) -> &'static str {
284 self.g.name
285 }
286
287 fn ident_char(&self, c: char) -> bool {
288 c.is_alphanumeric() || c == '_' || self.g.ident.contains(c)
289 }
290
291 fn ident_at(&self, t: &str, i: usize) -> Option<usize> {
293 let c = t[i..].chars().next()?;
294 if !(c.is_alphabetic() || c == '_') {
295 return None;
296 }
297 let mut e = i + c.len_utf8();
298 for ch in t[e..].chars() {
299 if !self.ident_char(ch) {
300 break;
301 }
302 e += ch.len_utf8();
303 }
304 Some(e)
305 }
306
307 fn prev_ident(&self, t: &str, i: usize) -> bool {
308 t[..i].chars().next_back().is_some_and(|c| self.ident_char(c))
309 }
310
311 pub fn line(&self, t: &str, state: u32, out: &mut Vec<Token>) -> u32 {
313 out.clear();
314 self.line_part(t, state, 0, out, &mut |_, _| false).1.unwrap_or(0)
315 }
316
317 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>) {
322 let mut i = from;
323 let bol = t.len() - t.trim_start_matches([' ', '\t']).len();
324 if from == 0 && state != 0 {
325 let (e, st) = self.resume(t, state, bol, out);
326 if st != 0 {
327 return (t.len(), Some(st));
328 }
329 i = e;
330 }
331 self.scan(t, i, bol, false, out, at)
332 }
333
334 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>) {
338 let b = t.as_bytes();
339 let n = b.len();
340 let rules = self.g.rules;
341 let mut braces = 0u32;
342 'outer: while i < n {
343 if !code && at(i, out) {
344 return (i, None);
345 }
346 if code && b[i] == b'{' {
347 braces += 1;
348 } else if code && b[i] == b'}' {
349 if braces == 0 {
350 return (i, Some(0));
351 }
352 braces -= 1;
353 }
354 let at_bol = i == bol;
355 let prev = self.prev_ident(t, i);
356 for (ri, rule) in rules.iter().enumerate() {
357 match rule {
358 Rule::LineComment(p) => {
359 if t[i..].starts_with(p) {
360 push(out, i, n, SY_COMMENT);
361 return (n, Some(0));
362 }
363 }
364 Rule::Block { open, close, kind, nested, bol: only } => {
365 if (!*only || at_bol) && t[i..].starts_with(open) && !(prev && open.starts_with(|c: char| c.is_alphanumeric())) {
366 if *only {
367 push(out, i, n, *kind);
368 return (n, Some(enc(ri, 1, 0)));
369 }
370 let (e, depth) = scan_block(t, i + open.len(), open, close, *nested, 1);
371 push(out, i, e, *kind);
372 if depth > 0 {
373 return (n, Some(enc(ri, depth, 0)));
374 }
375 i = e;
376 continue 'outer;
377 }
378 }
379 Rule::Str { open, close, escape, multi, kind } => {
380 if t[i..].starts_with(open) && !(prev && open.starts_with(|c: char| c.is_alphanumeric())) {
381 let (e, done) = scan_str(t, i, i + open.len(), close, *escape, *kind, out);
382 if !done && *multi {
383 return (n, Some(enc(ri, 0, 0)));
384 }
385 i = e;
386 continue 'outer;
387 }
388 }
389 Rule::RawStr => {
390 if let Some((q, hashes)) = raw_open(t, i).filter(|_| !prev) {
391 match raw_close(t, q, hashes) {
392 Some(e) => {
393 push(out, i, e, SY_STRING);
394 i = e;
395 continue 'outer;
396 }
397 None => {
398 push(out, i, n, SY_STRING);
399 return (n, Some(enc(ri, 0, hashes as u32)));
400 }
401 }
402 }
403 }
404 Rule::Interp { open, close, start } => {
405 if t[i..].starts_with(open) && !(prev && open.starts_with(|c: char| c.is_alphanumeric())) {
406 let (e, st) = self.interp(t, i, open.len(), close, start, bol, out);
407 if st != 0 {
408 return (n, Some(st));
409 }
410 i = e;
411 continue 'outer;
412 }
413 }
414 Rule::Words(list, kind) => {
415 if !prev {
416 if let Some(e) = self.ident_at(t, i) {
417 if list.contains(&&t[i..e]) {
418 push(out, i, e, *kind);
419 i = e;
420 continue 'outer;
421 }
422 }
423 }
424 }
425 Rule::Number => {
426 if !prev && b[i].is_ascii_digit() {
427 let e = number_end(t, i);
428 push(out, i, e, SY_NUMBER);
429 i = e;
430 continue 'outer;
431 }
432 }
433 Rule::Custom(f) => {
434 if let Some((len, kind)) = f(t, i, at_bol) {
435 if len > 0 {
436 let e = (i + len).min(n);
437 push(out, i, e, kind);
438 i = e;
439 while i < n && !t.is_char_boundary(i) {
440 i += 1;
441 }
442 continue 'outer;
443 }
444 }
445 }
446 Rule::Punct(chars) => {
447 if b[i] < 0x80 && chars.as_bytes().contains(&b[i]) {
448 push(out, i, i + 1, SY_PUNCT);
449 i += 1;
450 continue 'outer;
451 }
452 }
453 Rule::Nest { open, close, kind, inner } => {
454 if t[i..].starts_with(open) && !(prev && open.starts_with(|c: char| c.is_alphanumeric())) {
455 push(out, i, i + open.len(), *kind);
456 match Highlighter::new(*inner).nest_run(t, i + open.len(), 0, bol, close, out) {
457 Ok(e) => {
458 push(out, e, e + close.len(), *kind);
459 i = e + close.len();
460 continue 'outer;
461 }
462 Err(st) => return (n, Some(enc_in(ri, st))),
463 }
464 }
465 }
466 Rule::Element { name, tag, inner } => {
467 if let Some(gt) = element_start(t, i, name) {
468 push(out, i, i + 1, SY_PUNCT);
469 Highlighter::new(*tag).scan(&t[..gt], i + 1, bol, false, out, &mut |_, _| false);
470 push(out, gt, gt + 1, SY_PUNCT);
471 match Highlighter::new(*inner).element_run(t, gt + 1, 0, bol, name, out) {
472 Ok(e) => {
473 i = e;
474 continue 'outer;
475 }
476 Err(st) => return (n, Some(enc_in(ri, st))),
477 }
478 }
479 }
480 }
481 }
482 if !prev {
484 if let Some(e) = self.ident_at(t, i) {
485 let g = self.g;
486 let mut next = e;
487 while next < n && b[next] == b' ' {
488 next += 1;
489 }
490 let kind = if g.macros && b.get(e) == Some(&b'!') && b.get(e + 1) != Some(&b'=') {
491 SY_FUNC
492 } else if g.calls && b.get(next) == Some(&b'(') {
493 SY_FUNC
494 } else if g.caps_types && t[i..].chars().next().is_some_and(|c| c.is_uppercase()) {
495 SY_TYPE
496 } else {
497 SY_TEXT
498 };
499 if kind != SY_TEXT {
500 push(out, i, if kind == SY_FUNC && g.macros && b.get(e) == Some(&b'!') { e + 1 } else { e }, kind);
501 if kind == SY_FUNC && g.macros && b.get(e) == Some(&b'!') {
502 i = e + 1;
503 continue;
504 }
505 }
506 i = e;
507 continue;
508 }
509 }
510 i += utf8_len(b[i]);
511 }
512 (n, Some(0))
513 }
514
515 #[allow(clippy::too_many_arguments)]
518 fn interp(&self, t: &str, i: usize, open: usize, close: &str, start: &str, bol: usize, out: &mut Vec<Token>) -> (usize, u32) {
519 let b = t.as_bytes();
520 let mut s = i;
521 let mut j = i + open;
522 while j < b.len() {
523 if b[j] == b'\\' {
524 push(out, s, j, SY_STRING);
525 let mut e = (j + 1 + b.get(j + 1).map_or(0, |&c| utf8_len(c))).min(b.len());
526 if b.get(j + 1) == Some(&b'u') && b.get(j + 2) == Some(&b'{') {
527 e = j + 3 + b[j + 3..].iter().take_while(|c| c.is_ascii_hexdigit()).count();
528 if b.get(e) == Some(&b'}') {
529 e += 1;
530 }
531 }
532 while e < b.len() && !t.is_char_boundary(e) {
533 e += 1;
534 }
535 push(out, j, e, SY_ESCAPE);
536 j = e;
537 s = e;
538 continue;
539 }
540 if t[j..].starts_with(close) {
541 push(out, s, j + close.len(), SY_STRING);
542 return (j + close.len(), 0);
543 }
544 if t[j..].starts_with(start) {
545 push(out, s, j, SY_STRING);
546 push(out, j, j + start.len(), SY_META);
547 let (e, st) = self.scan(t, j + start.len(), bol, true, out, &mut |_, _| false);
548 let st = st.unwrap_or(0);
549 if st != 0 || e >= b.len() {
550 return (b.len(), st);
551 }
552 push(out, e, e + 1, SY_META);
553 j = e + 1;
554 s = j;
555 continue;
556 }
557 j += utf8_len(b[j]);
558 }
559 push(out, s, b.len(), SY_STRING);
560 (b.len(), 0)
561 }
562
563 fn resume(&self, t: &str, state: u32, bol: usize, out: &mut Vec<Token>) -> (usize, u32) {
565 let n = t.len();
566 let Some(ri) = ((state & 0xFF) as usize).checked_sub(1) else { return (0, 0) };
568 let Some(rule) = self.g.rules.get(ri) else { return (0, 0) };
569 match rule {
570 Rule::Block { open, close, kind, nested, bol: only } => {
571 if *only {
572 push(out, 0, n, *kind);
573 return if t[bol..].starts_with(close) { (n, 0) } else { (n, state) };
574 }
575 let (e, depth) = scan_block(t, 0, open, close, *nested, (state >> 8) & 0xFF);
576 push(out, 0, e, *kind);
577 if depth > 0 { (n, enc(ri, depth, 0)) } else { (e, 0) }
578 }
579 Rule::Str { close, escape, kind, .. } => {
580 let (e, done) = scan_str(t, 0, 0, close, *escape, *kind, out);
581 if done { (e, 0) } else { (n, state) }
582 }
583 Rule::Nest { close, kind, inner, .. } => match Highlighter::new(*inner).nest_run(t, 0, state >> 8, bol, close, out) {
584 Ok(e) => {
585 push(out, e, e + close.len(), *kind);
586 (e + close.len(), 0)
587 }
588 Err(st) => (n, enc_in(ri, st)),
589 },
590 Rule::Element { name, inner, .. } => match Highlighter::new(*inner).element_run(t, 0, state >> 8, bol, name, out) {
591 Ok(e) => (e, 0),
592 Err(st) => (n, enc_in(ri, st)),
593 },
594 Rule::RawStr => {
595 let hashes = (state >> 16) as usize;
596 match raw_close(t, 0, hashes) {
597 Some(e) => {
598 push(out, 0, e, SY_STRING);
599 (e, 0)
600 }
601 None => {
602 push(out, 0, n, SY_STRING);
603 (n, state)
604 }
605 }
606 }
607 _ => (0, 0),
608 }
609 }
610
611 fn nest_run(&self, t: &str, i: usize, st: u32, bol: usize, close: &str, out: &mut Vec<Token>) -> Result<usize, u32> {
615 let mut i = i;
616 if st != 0 {
617 let (e, s) = self.resume(t, st, bol, out);
618 if s != 0 {
619 return Err(s);
620 }
621 i = e;
622 }
623 let braces = close == "}" && !self.g.rules.iter().any(|r| matches!(r, Rule::Nest { open, .. } if open.starts_with('{')));
625 let mut depth = 0u32;
626 let (e, s) = self.scan(t, i, bol, false, out, &mut |p, _| {
627 let r = &t[p..];
628 if braces && r.starts_with('{') {
629 depth += 1;
630 } else if r.starts_with(close) {
631 if depth == 0 {
632 return true;
633 }
634 depth -= 1;
635 }
636 false
637 });
638 match s {
639 None => Ok(e),
640 Some(s) => Err(s),
641 }
642 }
643
644 fn element_run(&self, t: &str, i: usize, st: u32, bol: usize, name: &str, out: &mut Vec<Token>) -> Result<usize, u32> {
648 let end = find_end_tag(t, i, name);
649 let part = &t[..end.unwrap_or(t.len())];
650 let mut i = i;
651 if st != 0 {
652 let (e, s) = self.resume(part, st, bol, out);
653 if s != 0 {
654 return end.ok_or(s);
655 }
656 i = e;
657 }
658 let (_, s) = self.scan(part, i, bol, false, out, &mut |_, _| false);
659 end.ok_or(s.unwrap_or(0))
660 }
661
662 pub fn tokens(&self, text: &str) -> Vec<Vec<Token>> {
664 let mut st = 0;
665 let mut out = Vec::new();
666 for l in crate::rich_doc::lines_of(text) {
667 let mut v = Vec::new();
668 st = self.line(l, st, &mut v);
669 out.push(v);
670 }
671 out
672 }
673}
674
675fn scan_block(t: &str, i: usize, open: &str, close: &str, nested: bool, depth: u32) -> (usize, u32) {
677 let mut d = depth.max(1);
680 let mut j = i;
681 while j < t.len() {
682 if t[j..].starts_with(close) {
683 d -= 1;
684 j += close.len();
685 if d == 0 {
686 return (j, 0);
687 }
688 continue;
689 }
690 if nested && t[j..].starts_with(open) {
691 d += 1;
692 j += open.len();
693 continue;
694 }
695 j += utf8_len(t.as_bytes()[j]);
696 }
697 (t.len(), d.max(1))
698}
699
700fn scan_str(t: &str, start: usize, i: usize, close: &str, escape: bool, kind: u8, out: &mut Vec<Token>) -> (usize, bool) {
702 let b = t.as_bytes();
703 let mut s = start;
704 let mut j = i;
705 while j < b.len() {
706 if escape && b[j] == b'\\' {
707 push(out, s, j, kind);
708 let mut e = (j + 1 + b.get(j + 1).map_or(0, |&c| utf8_len(c))).min(b.len());
709 if b.get(j + 1) == Some(&b'u') && b.get(j + 2) == Some(&b'{') {
710 e = t[j..].find('}').map_or(b.len(), |k| j + k + 1);
711 } else if b.get(j + 1) == Some(&b'x') {
712 e = (j + 4).min(b.len());
713 } else if b.get(j + 1) == Some(&b'u') && kind == SY_STRING {
714 e = (j + 6).min(b.len());
715 }
716 while e < b.len() && !t.is_char_boundary(e) {
717 e += 1;
718 }
719 push(out, j, e, SY_ESCAPE);
720 j = e;
721 s = e;
722 continue;
723 }
724 if t[j..].starts_with(close) {
725 let e = j + close.len();
726 push(out, s, e, kind);
727 return (e, true);
728 }
729 j += utf8_len(b[j]);
730 }
731 push(out, s, b.len(), kind);
732 (b.len(), false)
733}
734
735fn raw_open(t: &str, i: usize) -> Option<(usize, usize)> {
737 let b = t.as_bytes();
738 let mut j = i;
739 if b.get(j) == Some(&b'b') || b.get(j) == Some(&b'c') {
740 j += 1;
741 }
742 if b.get(j) != Some(&b'r') {
743 return None;
744 }
745 j += 1;
746 let mut h = 0;
747 while b.get(j) == Some(&b'#') {
748 h += 1;
749 j += 1;
750 }
751 (b.get(j) == Some(&b'"')).then_some((j + 1, h))
752}
753
754fn raw_close(t: &str, i: usize, hashes: usize) -> Option<usize> {
756 let b = t.as_bytes();
757 let mut j = i;
758 while j < b.len() {
759 if b[j] == b'"' && b[j + 1..].iter().take(hashes).filter(|&&c| c == b'#').count() == hashes && b.len() >= j + 1 + hashes {
760 return Some(j + 1 + hashes);
761 }
762 j += 1;
763 }
764 None
765}
766
767fn number_end(t: &str, i: usize) -> usize {
769 let b = t.as_bytes();
770 let mut j = i;
771 let hex = b[i] == b'0' && matches!(b.get(i + 1), Some(b'x') | Some(b'X'));
772 if hex || (b[i] == b'0' && matches!(b.get(i + 1), Some(b'b') | Some(b'o') | Some(b'B') | Some(b'O'))) {
773 j += 2;
774 }
775 while j < b.len() {
776 let c = b[j];
777 if c.is_ascii_alphanumeric() || c == b'_' {
778 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()) {
780 j += 2;
781 continue;
782 }
783 j += 1;
784 } else if c == b'.' && b.get(j + 1).is_some_and(|d| d.is_ascii_digit()) && !t[i..j].contains('.') {
785 j += 1;
786 } else {
787 break;
788 }
789 }
790 j
791}
792
793fn rust_quote(t: &str, i: usize, _: bool) -> Option<(usize, u8)> {
797 let b = t.as_bytes();
798 if b[i] != b'\'' {
799 return None;
800 }
801 if b.get(i + 1) == Some(&b'\\') {
802 let e = t[i + 2..].find('\'').map_or(t.len(), |k| i + 2 + k + 1);
803 return Some((e - i, SY_STRING));
804 }
805 let c = t[i + 1..].chars().next()?;
806 let after = i + 1 + c.len_utf8();
807 if b.get(after) == Some(&b'\'') {
808 return Some((after + 1 - i, SY_STRING));
809 }
810 if c.is_alphabetic() || c == '_' {
811 let mut e = after;
812 for ch in t[after..].chars() {
813 if !(ch.is_alphanumeric() || ch == '_') {
814 break;
815 }
816 e += ch.len_utf8();
817 }
818 return Some((e - i, SY_META));
819 }
820 None
821}
822
823fn rust_attr(t: &str, i: usize, _: bool) -> Option<(usize, u8)> {
825 let r = &t[i..];
826 let start = if r.starts_with("#[") {
827 2
828 } else if r.starts_with("#![") {
829 3
830 } else {
831 return None;
832 };
833 let mut d = 1;
834 let mut in_str = false;
835 for (k, c) in r[start..].char_indices() {
836 match c {
837 '"' => in_str = !in_str,
838 '[' if !in_str => d += 1,
839 ']' if !in_str => {
840 d -= 1;
841 if d == 0 {
842 return Some((start + k + 1, SY_META));
843 }
844 }
845 _ => {}
846 }
847 }
848 Some((r.len(), SY_META))
849}
850
851fn json_string(t: &str, i: usize, _: bool) -> Option<(usize, u8)> {
853 let b = t.as_bytes();
854 if b[i] != b'"' {
855 return None;
856 }
857 let mut j = i + 1;
858 while j < b.len() {
859 match b[j] {
860 b'\\' => j += 2,
861 b'"' => {
862 j += 1;
863 let rest = t[j.min(t.len())..].trim_start();
864 return Some((j - i, if rest.starts_with(':') { SY_KEY } else { SY_STRING }));
865 }
866 _ => j += 1,
867 }
868 }
869 Some((b.len() - i, SY_STRING))
870}
871
872fn ini_section(t: &str, i: usize, bol: bool) -> Option<(usize, u8)> {
874 if !bol || t.as_bytes()[i] != b'[' {
875 return None;
876 }
877 let e = t[i..].find(']').map_or(t.len(), |k| {
878 let mut e = i + k + 1;
879 if t[e..].starts_with(']') {
880 e += 1;
881 }
882 e
883 });
884 Some((e - i, SY_HEADING))
885}
886
887fn ini_key(t: &str, i: usize, bol: bool) -> Option<(usize, u8)> {
889 if !bol {
890 return None;
891 }
892 let r = &t[i..];
893 let c = r.chars().next()?;
894 if matches!(c, '[' | ';' | '#' | '=') {
895 return None;
896 }
897 let sep = r.find(['=', ':'])?;
898 let key = r[..sep].trim_end();
899 if key.is_empty() || key.contains(['"', '\'']) && !key.starts_with(['"', '\'']) {
900 return None;
901 }
902 Some((key.len(), SY_KEY))
903}
904
905fn url_len(r: &str) -> Option<usize> {
907 if !(r.starts_with("http://") || r.starts_with("https://")) {
908 return None;
909 }
910 let e = r.find(|c: char| c.is_whitespace() || matches!(c, ')' | '>' | '"' | '\'' | ']')).unwrap_or(r.len());
911 Some(e)
912}
913
914fn md_heading(t: &str, i: usize, bol: bool) -> Option<(usize, u8)> {
916 if !bol {
917 return None;
918 }
919 let r = &t[i..];
920 let h = r.bytes().take_while(|&c| c == b'#').count();
921 if h == 0 || h > 6 || !(r.len() == h || r.as_bytes()[h] == b' ') {
922 return None;
923 }
924 Some((r.len(), SY_HEADING))
925}
926
927fn md_quote(t: &str, i: usize, bol: bool) -> Option<(usize, u8)> {
929 (bol && t.as_bytes()[i] == b'>').then_some((t.len() - i, SY_COMMENT))
930}
931
932fn md_rule(t: &str, i: usize, bol: bool) -> Option<(usize, u8)> {
934 if !bol {
935 return None;
936 }
937 let r = t[i..].trim_end();
938 let c = r.bytes().next()?;
939 if !matches!(c, b'-' | b'*' | b'_') {
940 return None;
941 }
942 let n = r.bytes().filter(|&x| x == c).count();
943 (n >= 3 && r.bytes().all(|x| x == c || x == b' ')).then_some((t.len() - i, SY_PUNCT))
944}
945
946fn md_list(t: &str, i: usize, bol: bool) -> Option<(usize, u8)> {
948 if !bol {
949 return None;
950 }
951 let b = &t.as_bytes()[i..];
952 if b.len() >= 2 && matches!(b[0], b'-' | b'*' | b'+') && b[1] == b' ' {
953 return Some((1, SY_KEYWORD));
954 }
955 let d = b.iter().take_while(|c| c.is_ascii_digit()).count();
956 if d > 0 && d < 10 && b.len() > d + 1 && matches!(b[d], b'.' | b')') && b[d + 1] == b' ' {
957 return Some((d + 1, SY_KEYWORD));
958 }
959 None
960}
961
962fn md_emph(t: &str, i: usize, _: bool) -> Option<(usize, u8)> {
964 let b = t.as_bytes();
965 let c = b[i];
966 if c != b'*' && c != b'_' {
967 return None;
968 }
969 if c == b'_' && t[..i].chars().next_back().is_some_and(|p| p.is_alphanumeric()) {
970 return None;
971 }
972 let strong = b.get(i + 1) == Some(&c);
973 let delim = if strong { 2 } else { 1 };
974 let inner = i + delim;
975 if b.get(inner).is_none_or(|x| *x == b' ' || *x == c) {
976 return None;
977 }
978 let pat: &[u8] = if strong { &b[i..i + 2] } else { &b[i..i + 1] };
979 let mut j = inner + 1;
980 while j + delim <= b.len() {
981 if &b[j..j + delim] == pat && b[j - 1] != b' ' && (strong || b.get(j + 1) != Some(&c)) {
982 if c == b'_' && t[j + delim..].chars().next().is_some_and(|p| p.is_alphanumeric()) {
983 j += 1;
984 continue;
985 }
986 return Some((j + delim - i, if strong { SY_STRONG } else { SY_EMPH }));
987 }
988 j += 1;
989 }
990 None
991}
992
993fn md_link(t: &str, i: usize, _: bool) -> Option<(usize, u8)> {
995 let r = &t[i..];
996 if let Some(e) = url_len(r) {
997 return Some((e, SY_LINK));
998 }
999 if r.starts_with('<') {
1000 let e = r.find('>')?;
1001 return url_len(&r[1..]).filter(|&u| u + 1 == e).map(|_| (e + 1, SY_LINK));
1002 }
1003 let s = if r.starts_with("![") { 1 } else if r.starts_with('[') { 0 } else { return None };
1004 let close = r[s..].find("](")? + s;
1005 let end = r[close + 2..].find(')')? + close + 3;
1006 Some((end, SY_LINK))
1007}
1008
1009fn md_escape(t: &str, i: usize, _: bool) -> Option<(usize, u8)> {
1011 let b = t.as_bytes();
1012 (b[i] == b'\\' && b.get(i + 1).is_some_and(|c| c.is_ascii_punctuation())).then_some((2, SY_ESCAPE))
1013}
1014
1015fn toml_date(t: &str, i: usize, _: bool) -> Option<(usize, u8)> {
1017 let b = &t.as_bytes()[i..];
1018 if b.len() < 5 || !b[0].is_ascii_digit() {
1019 return None;
1020 }
1021 let date = b.len() >= 10 && b[4] == b'-' && b[7] == b'-';
1022 let time = b.len() >= 8 && b[2] == b':' && b[5] == b':';
1023 if !date && !time {
1024 return None;
1025 }
1026 let e = b.iter().position(|c| !(c.is_ascii_alphanumeric() || matches!(c, b'-' | b':' | b'.' | b'+'))).unwrap_or(b.len());
1027 Some((e, SY_NUMBER))
1028}
1029
1030const RUST_KEYWORDS: &[&str] = &[
1033 "as", "async", "await", "break", "const", "continue", "crate", "dyn", "else", "enum", "extern", "fn", "for", "if", "impl", "in", "let", "loop",
1034 "match", "mod", "move", "mut", "pub", "ref", "return", "self", "static", "struct", "super", "trait", "type", "union", "unsafe", "use", "where",
1035 "while", "yield", "macro_rules", "try", "gen",
1036];
1037const RUST_TYPES: &[&str] = &[
1038 "u8", "u16", "u32", "u64", "u128", "usize", "i8", "i16", "i32", "i64", "i128", "isize", "f32", "f64", "bool", "char", "str", "Self",
1039];
1040const RUST_CONSTS: &[&str] = &["true", "false", "None", "Some", "Ok", "Err"];
1041
1042static RUST_RULES: [Rule; 12] = [
1043 Rule::LineComment("//"),
1044 Rule::Block { open: "/*", close: "*/", kind: SY_COMMENT, nested: true, bol: false },
1045 Rule::RawStr,
1046 Rule::Str { open: "b\"", close: "\"", escape: true, multi: true, kind: SY_STRING },
1047 Rule::Str { open: "\"", close: "\"", escape: true, multi: true, kind: SY_STRING },
1048 Rule::Custom(rust_quote),
1049 Rule::Custom(rust_attr),
1050 Rule::Words(RUST_KEYWORDS, SY_KEYWORD),
1051 Rule::Words(RUST_TYPES, SY_TYPE),
1052 Rule::Words(RUST_CONSTS, SY_CONST),
1053 Rule::Number,
1054 Rule::Punct("+-*/%=<>!&|^~?:;,.()[]{}@#$"),
1055];
1056
1057pub static RUST: Grammar = Grammar {
1059 name: "Rust",
1060 exts: &["rs"],
1061 rules: &RUST_RULES,
1062 ident: "",
1063 calls: true,
1064 macros: true,
1065 caps_types: true,
1066 comment: "//",
1067 openers: "{[(",
1068 closers: "}])",
1069 pairs: "()[]{}",
1070};
1071
1072static JSON_RULES: [Rule; 6] = [
1073 Rule::LineComment("//"),
1074 Rule::Block { open: "/*", close: "*/", kind: SY_COMMENT, nested: false, bol: false },
1075 Rule::Custom(json_string),
1076 Rule::Words(&["true", "false", "null"], SY_CONST),
1077 Rule::Number,
1078 Rule::Punct("{}[],:-"),
1079];
1080
1081pub static JSON: Grammar = Grammar {
1083 name: "JSON",
1084 exts: &["json", "jsonc", "geojson"],
1085 rules: &JSON_RULES,
1086 ident: "",
1087 calls: false,
1088 macros: false,
1089 caps_types: false,
1090 comment: "//",
1091 openers: "{[",
1092 closers: "}]",
1093 pairs: "[]{}",
1094};
1095
1096const INI_CONSTS: &[&str] = &["true", "false", "yes", "no", "on", "off", "TRUE", "FALSE", "Yes", "No", "On", "Off", "True", "False"];
1097
1098fn ini_comment(t: &str, i: usize, bol: bool) -> Option<(usize, u8)> {
1099 (bol && matches!(t.as_bytes()[i], b';' | b'#')).then_some((t.len() - i, SY_COMMENT))
1100}
1101
1102static INI_RULES: [Rule; 8] = [
1103 Rule::Custom(ini_comment),
1104 Rule::Custom(ini_section),
1105 Rule::Custom(ini_key),
1106 Rule::Str { open: "\"", close: "\"", escape: true, multi: false, kind: SY_STRING },
1107 Rule::Str { open: "'", close: "'", escape: false, multi: false, kind: SY_STRING },
1108 Rule::Words(INI_CONSTS, SY_CONST),
1109 Rule::Number,
1110 Rule::Punct("=:,[]"),
1111];
1112
1113pub static INI: Grammar = Grammar {
1115 name: "INI",
1116 exts: &["ini", "cfg", "conf", "desktop", "properties", "inf", "reg"],
1117 rules: &INI_RULES,
1118 ident: "-.",
1119 calls: false,
1120 macros: false,
1121 caps_types: false,
1122 comment: ";",
1123 openers: "",
1124 closers: "",
1125 pairs: "[]",
1126};
1127
1128static TOML_RULES: [Rule; 12] = [
1129 Rule::LineComment("#"),
1130 Rule::Custom(ini_section),
1131 Rule::Custom(ini_key),
1132 Rule::Str { open: "\"\"\"", close: "\"\"\"", escape: true, multi: true, kind: SY_STRING },
1133 Rule::Str { open: "'''", close: "'''", escape: false, multi: true, kind: SY_STRING },
1134 Rule::Str { open: "\"", close: "\"", escape: true, multi: false, kind: SY_STRING },
1135 Rule::Str { open: "'", close: "'", escape: false, multi: false, kind: SY_STRING },
1136 Rule::Words(&["true", "false", "inf", "nan"], SY_CONST),
1137 Rule::Custom(toml_date),
1138 Rule::Number,
1139 Rule::Punct("=,.[]{}+-"),
1140 Rule::Punct(":"),
1141];
1142
1143pub static TOML: Grammar = Grammar {
1145 name: "TOML",
1146 exts: &["toml", "lock"],
1147 rules: &TOML_RULES,
1148 ident: "-",
1149 calls: false,
1150 macros: false,
1151 caps_types: false,
1152 comment: "#",
1153 openers: "[{",
1154 closers: "]}",
1155 pairs: "[]{}",
1156};
1157
1158static MD_RULES: [Rule; 11] = [
1159 Rule::Block { open: "```", close: "```", kind: SY_CODE, nested: false, bol: true },
1160 Rule::Block { open: "~~~", close: "~~~", kind: SY_CODE, nested: false, bol: true },
1161 Rule::Block { open: "<!--", close: "-->", kind: SY_COMMENT, nested: false, bol: false },
1162 Rule::Custom(md_heading),
1163 Rule::Custom(md_quote),
1164 Rule::Custom(md_rule),
1165 Rule::Custom(md_list),
1166 Rule::Custom(md_escape),
1167 Rule::Str { open: "`", close: "`", escape: false, multi: false, kind: SY_CODE },
1168 Rule::Custom(md_emph),
1169 Rule::Custom(md_link),
1170];
1171
1172pub static MARKDOWN: Grammar = Grammar {
1175 name: "Markdown",
1176 exts: &["md", "markdown", "mdown"],
1177 rules: &MD_RULES,
1178 ident: "",
1179 calls: false,
1180 macros: false,
1181 caps_types: false,
1182 comment: "",
1183 openers: "",
1184 closers: "",
1185 pairs: "()[]",
1186};
1187
1188const JFS_KEYWORDS: &[&str] = &["let", "fn", "if", "else", "while", "for", "in", "break", "continue", "return", "try", "catch", "throw"];
1192
1193static JFS_RULES: [Rule; 7] = [
1194 Rule::LineComment("//"),
1195 Rule::Block { open: "/*", close: "*/", kind: SY_COMMENT, nested: false, bol: false },
1196 Rule::Interp { open: "\"", close: "\"", start: "${" },
1197 Rule::Words(JFS_KEYWORDS, SY_KEYWORD),
1198 Rule::Words(&["true", "false", "nil"], SY_CONST),
1199 Rule::Number,
1200 Rule::Punct("+-*/%=<>!&|?:;,.()[]{}"),
1201];
1202
1203pub static JFS: Grammar = Grammar {
1206 name: "JFS",
1207 exts: &["jfs"],
1208 rules: &JFS_RULES,
1209 ident: "",
1210 calls: true,
1211 macros: false,
1212 caps_types: false,
1213 comment: "//",
1214 openers: "{[(",
1215 closers: "}])",
1216 pairs: "()[]{}",
1217};
1218
1219fn js_regex(t: &str, i: usize, _: bool) -> Option<(usize, u8)> {
1224 let b = t.as_bytes();
1225 if b[i] != b'/' || matches!(b.get(i + 1), Some(b'/' | b'*')) {
1226 return None;
1227 }
1228 let before = t[..i].trim_end();
1229 let ok = match before.as_bytes().last() {
1230 None => true,
1231 Some(b')' | b']' | b'}' | b'"' | b'\'' | b'`' | b'.') => false,
1232 Some(&c) if c.is_ascii_alphanumeric() || c == b'_' || c == b'$' || c >= 0x80 => {
1233 let w = before.rsplit(|c: char| !(c.is_alphanumeric() || c == '_' || c == '$')).next().unwrap_or("");
1234 matches!(w, "return" | "typeof" | "instanceof" | "in" | "of" | "new" | "delete" | "void" | "throw" | "case" | "do" | "else" | "yield" | "await")
1235 }
1236 Some(_) => true,
1237 };
1238 if !ok {
1239 return None;
1240 }
1241 let mut j = i + 1;
1242 let mut class = false;
1243 while j < b.len() {
1244 match b[j] {
1245 b'\\' => j += 2,
1246 b'[' => {
1247 class = true;
1248 j += 1;
1249 }
1250 b']' => {
1251 class = false;
1252 j += 1;
1253 }
1254 b'/' if !class => {
1255 j += 1;
1256 while j < b.len() && b[j].is_ascii_alphabetic() {
1257 j += 1;
1258 }
1259 return Some((j - i, SY_STRING));
1260 }
1261 _ => j += 1,
1262 }
1263 }
1264 None
1265}
1266
1267fn js_ident_len(t: &str, i: usize) -> usize {
1269 let mut e = i;
1270 for (k, c) in t[i..].char_indices() {
1271 if !(c.is_alphanumeric() || c == '_' || c == '$') || (k == 0 && c.is_ascii_digit()) {
1272 break;
1273 }
1274 e = i + k + c.len_utf8();
1275 }
1276 e - i
1277}
1278
1279fn js_member(t: &str, i: usize, _: bool) -> Option<(usize, u8)> {
1282 let member = t[..i].ends_with('.') && !t[..i].ends_with("..");
1283 let dollar = t.as_bytes()[i] == b'$' && !t[..i].chars().next_back().is_some_and(|c| c.is_alphanumeric() || c == '_' || c == '$');
1284 if !member && !dollar {
1285 return None;
1286 }
1287 let n = js_ident_len(t, i);
1288 if n == 0 {
1289 return None;
1290 }
1291 let call = t[i + n..].trim_start_matches(' ').starts_with('(');
1292 Some((n, if call { SY_FUNC } else { SY_TEXT }))
1293}
1294
1295fn js_tpl_text(t: &str, i: usize, _: bool) -> Option<(usize, u8)> {
1297 let r = &t.as_bytes()[i..];
1298 let mut j = 0;
1299 while j < r.len() && r[j] != b'`' && r[j] != b'\\' && !(r[j] == b'$' && r.get(j + 1) == Some(&b'{')) {
1300 j += 1;
1301 }
1302 (j > 0).then_some((j, SY_STRING))
1303}
1304
1305fn js_tpl_escape(t: &str, i: usize, _: bool) -> Option<(usize, u8)> {
1307 (t.as_bytes()[i] == b'\\').then(|| (1 + t[i + 1..].chars().next().map_or(0, |c| c.len_utf8()), SY_ESCAPE))
1308}
1309
1310const JS_KEYWORDS: &[&str] = &[
1311 "async", "await", "break", "case", "catch", "class", "const", "continue", "debugger", "default", "delete", "do", "else", "export", "extends",
1312 "finally", "for", "from", "function", "if", "import", "in", "instanceof", "let", "new", "of", "return", "static", "super", "switch", "this",
1313 "throw", "try", "typeof", "var", "void", "while", "with", "yield",
1314];
1315const JS_CONSTS: &[&str] = &["true", "false", "null", "undefined", "NaN", "Infinity"];
1316
1317static JS_TEMPLATE_RULES: [Rule; 3] = [
1318 Rule::Custom(js_tpl_escape),
1319 Rule::Nest { open: "${", close: "}", kind: SY_META, inner: &JAVASCRIPT },
1320 Rule::Custom(js_tpl_text),
1321];
1322
1323static JS_TEMPLATE: Grammar = Grammar {
1325 name: "JavaScript template",
1326 exts: &[],
1327 rules: &JS_TEMPLATE_RULES,
1328 ident: "",
1329 calls: false,
1330 macros: false,
1331 caps_types: false,
1332 comment: "",
1333 openers: "",
1334 closers: "",
1335 pairs: "{}",
1336};
1337
1338static JS_RULES: [Rule; 11] = [
1339 Rule::LineComment("//"),
1340 Rule::Block { open: "/*", close: "*/", kind: SY_COMMENT, nested: false, bol: false },
1341 Rule::Str { open: "\"", close: "\"", escape: true, multi: false, kind: SY_STRING },
1342 Rule::Str { open: "'", close: "'", escape: true, multi: false, kind: SY_STRING },
1343 Rule::Nest { open: "`", close: "`", kind: SY_STRING, inner: &JS_TEMPLATE },
1344 Rule::Custom(js_regex),
1345 Rule::Custom(js_member),
1346 Rule::Words(JS_KEYWORDS, SY_KEYWORD),
1347 Rule::Words(JS_CONSTS, SY_CONST),
1348 Rule::Number,
1349 Rule::Punct("+-*/%=<>!&|^~?:;,.()[]{}@#"),
1350];
1351
1352pub static JAVASCRIPT: Grammar = Grammar {
1357 name: "JavaScript",
1358 exts: &["js", "mjs", "cjs"],
1359 rules: &JS_RULES,
1360 ident: "$",
1361 calls: true,
1362 macros: false,
1363 caps_types: true,
1364 comment: "//",
1365 openers: "{[(",
1366 closers: "}])",
1367 pairs: "()[]{}",
1368};
1369
1370fn css_in_selector(t: &str, i: usize) -> bool {
1373 t[i..].find(['{', ';', '}']).is_some_and(|k| t.as_bytes()[i + k] == b'{')
1374}
1375
1376fn css_name_len(b: &[u8], i: usize) -> usize {
1377 b[i..].iter().take_while(|&&c| c.is_ascii_alphanumeric() || c == b'-' || c == b'_' || c >= 0x80).count()
1378}
1379
1380fn css_prop(t: &str, i: usize, _: bool) -> Option<(usize, u8)> {
1382 let b = t.as_bytes();
1383 if !(b[i].is_ascii_alphabetic() || b[i] == b'-' || b[i] == b'_') {
1384 return None;
1385 }
1386 let before = t[..i].trim_end_matches([' ', '\t']);
1387 if !(before.is_empty() || before.ends_with(['{', ';'])) {
1388 return None;
1389 }
1390 let e = i + css_name_len(b, i);
1391 if !t[e..].trim_start_matches([' ', '\t']).starts_with(':') || css_in_selector(t, e) {
1392 return None;
1393 }
1394 Some((e - i, SY_KEY))
1395}
1396
1397fn css_at(t: &str, i: usize, _: bool) -> Option<(usize, u8)> {
1399 let b = t.as_bytes();
1400 if b[i] != b'@' {
1401 return None;
1402 }
1403 let n = css_name_len(b, i + 1);
1404 (n > 0).then_some((n + 1, SY_KEYWORD))
1405}
1406
1407fn css_sel(t: &str, i: usize, block: bool) -> Option<(usize, u8)> {
1410 let b = t.as_bytes();
1411 let start = |k: usize| b.get(k).is_some_and(|&x| x.is_ascii_alphabetic() || x == b'-' || x == b'_' || x >= 0x80);
1412 match b[i] {
1413 b'.' if start(i + 1) && !t[..i].chars().next_back().is_some_and(|p| p.is_ascii_digit()) => Some((1 + css_name_len(b, i + 1), SY_TYPE)),
1414 b'#' => {
1415 let n = css_name_len(b, i + 1);
1416 if n == 0 {
1417 return None;
1418 }
1419 let hex = matches!(n, 3 | 4 | 6 | 8) && b[i + 1..i + 1 + n].iter().all(|x| x.is_ascii_hexdigit());
1420 Some((1 + n, if block && hex && !css_in_selector(t, i) { SY_NUMBER } else { SY_CONST }))
1421 }
1422 b':' => {
1423 let k = if b.get(i + 1) == Some(&b':') { i + 2 } else { i + 1 };
1424 if !start(k) || (block && !css_in_selector(t, i)) {
1425 return None;
1426 }
1427 Some((k - i + css_name_len(b, k), SY_META))
1428 }
1429 _ => None,
1430 }
1431}
1432
1433fn css_sel_top(t: &str, i: usize, _: bool) -> Option<(usize, u8)> {
1434 css_sel(t, i, false)
1435}
1436
1437fn css_sel_block(t: &str, i: usize, _: bool) -> Option<(usize, u8)> {
1438 css_sel(t, i, true)
1439}
1440
1441fn css_value(t: &str, i: usize, _: bool) -> Option<(usize, u8)> {
1443 let b = t.as_bytes();
1444 if b[i] == b'!' {
1445 let r = t[i + 1..].trim_start_matches(' ');
1446 let k = t.len() - r.len();
1447 return (r.len() >= 9 && r.as_bytes()[..9].eq_ignore_ascii_case(b"important")).then_some((k + 9 - i, SY_KEYWORD));
1448 }
1449 if t[..i].chars().next_back().is_some_and(|p| p.is_alphanumeric() || p == '_' || p == '-' || p == '.') {
1450 return None;
1451 }
1452 let mut j = i;
1453 if matches!(b[j], b'-' | b'+') {
1454 j += 1;
1455 }
1456 let digit = |k: usize| b.get(k).is_some_and(|x| x.is_ascii_digit());
1457 if !(digit(j) || (b.get(j) == Some(&b'.') && digit(j + 1))) {
1458 return None;
1459 }
1460 while j < b.len() && (b[j].is_ascii_digit() || b[j] == b'.') {
1461 j += 1;
1462 }
1463 if matches!(b.get(j), Some(b'e' | b'E')) && (digit(j + 1) || (matches!(b.get(j + 1), Some(b'-' | b'+')) && digit(j + 2))) {
1464 j += 2;
1465 while j < b.len() && b[j].is_ascii_digit() {
1466 j += 1;
1467 }
1468 }
1469 if b.get(j) == Some(&b'%') {
1470 j += 1;
1471 } else {
1472 while j < b.len() && b[j].is_ascii_alphabetic() {
1473 j += 1;
1474 }
1475 }
1476 Some((j - i, SY_NUMBER))
1477}
1478
1479static CSS_RULES: [Rule; 8] = [
1480 Rule::Block { open: "/*", close: "*/", kind: SY_COMMENT, nested: false, bol: false },
1481 Rule::Str { open: "\"", close: "\"", escape: true, multi: false, kind: SY_STRING },
1482 Rule::Str { open: "'", close: "'", escape: true, multi: false, kind: SY_STRING },
1483 Rule::Custom(css_at),
1484 Rule::Nest { open: "{", close: "}", kind: SY_PUNCT, inner: &CSS_BLOCK },
1485 Rule::Custom(css_sel_top),
1486 Rule::Custom(css_value),
1487 Rule::Punct("{}()[],:;>+~*=|^$!/"),
1488];
1489
1490static CSS_BLOCK_RULES: [Rule; 9] = [
1491 Rule::Block { open: "/*", close: "*/", kind: SY_COMMENT, nested: false, bol: false },
1492 Rule::Str { open: "\"", close: "\"", escape: true, multi: false, kind: SY_STRING },
1493 Rule::Str { open: "'", close: "'", escape: true, multi: false, kind: SY_STRING },
1494 Rule::Custom(css_at),
1495 Rule::Nest { open: "{", close: "}", kind: SY_PUNCT, inner: &CSS_BLOCK },
1496 Rule::Custom(css_prop),
1497 Rule::Custom(css_sel_block),
1498 Rule::Custom(css_value),
1499 Rule::Punct("{}()[],:;>+~*=|^$!/"),
1500];
1501
1502static CSS_BLOCK: Grammar = Grammar {
1504 name: "CSS block",
1505 exts: &[],
1506 rules: &CSS_BLOCK_RULES,
1507 ident: "-",
1508 calls: true,
1509 macros: false,
1510 caps_types: false,
1511 comment: "",
1512 openers: "{",
1513 closers: "}",
1514 pairs: "()[]{}",
1515};
1516
1517pub static CSS: Grammar = Grammar {
1521 name: "CSS",
1522 exts: &["css"],
1523 rules: &CSS_RULES,
1524 ident: "-",
1525 calls: true,
1526 macros: false,
1527 caps_types: false,
1528 comment: "",
1529 openers: "{",
1530 closers: "}",
1531 pairs: "()[]{}",
1532};
1533
1534fn html_name_char(c: u8) -> bool {
1535 c.is_ascii_alphanumeric() || matches!(c, b'-' | b'_' | b':' | b'.') || c >= 0x80
1536}
1537
1538fn html_tag_name(t: &str, i: usize, _: bool) -> Option<(usize, u8)> {
1540 let b = t.as_bytes();
1541 if !(t[..i].ends_with('<') || t[..i].ends_with("</")) || !b[i].is_ascii_alphabetic() {
1542 return None;
1543 }
1544 Some((b[i..].iter().take_while(|&&c| html_name_char(c)).count(), SY_KEYWORD))
1545}
1546
1547fn html_bare_value(t: &str, i: usize, _: bool) -> Option<(usize, u8)> {
1549 let b = t.as_bytes();
1550 if !t[..i].trim_end_matches([' ', '\t']).ends_with('=') || matches!(b[i], b'"' | b'\'' | b'>' | b' ' | b'\t') {
1551 return None;
1552 }
1553 Some((b[i..].iter().take_while(|&&c| !matches!(c, b' ' | b'\t' | b'>')).count(), SY_STRING))
1554}
1555
1556fn html_attr(t: &str, i: usize, _: bool) -> Option<(usize, u8)> {
1558 let b = t.as_bytes();
1559 let n = b[i..].iter().take_while(|&&c| !matches!(c, b' ' | b'\t' | b'"' | b'\'' | b'>' | b'/' | b'=' | b'<')).count();
1560 (n > 0).then_some((n, SY_KEY))
1561}
1562
1563fn html_decl(t: &str, i: usize, _: bool) -> Option<(usize, u8)> {
1565 let r = &t[i..];
1566 if !(r.starts_with("<!") || r.starts_with("<?")) || r.starts_with("<!--") || r.starts_with("<![CDATA[") {
1567 return None;
1568 }
1569 Some((r.find('>').map_or(r.len(), |k| k + 1), SY_META))
1570}
1571
1572fn html_lone_lt(t: &str, i: usize, _: bool) -> Option<(usize, u8)> {
1574 let b = t.as_bytes();
1575 if b[i] != b'<' {
1576 return None;
1577 }
1578 let next = b.get(i + 1).copied();
1579 let tag = next.is_some_and(|c| c.is_ascii_alphabetic()) || (next == Some(b'/') && b.get(i + 2).is_some_and(|c| c.is_ascii_alphabetic()));
1580 (!tag).then_some((1, SY_TEXT))
1581}
1582
1583fn html_entity(t: &str, i: usize, _: bool) -> Option<(usize, u8)> {
1585 let b = t.as_bytes();
1586 if b[i] != b'&' {
1587 return None;
1588 }
1589 let (s, kind) = match (b.get(i + 1), b.get(i + 2)) {
1590 (Some(b'#'), Some(b'x' | b'X')) => (i + 3, 2),
1591 (Some(b'#'), _) => (i + 2, 1),
1592 _ => (i + 1, 0),
1593 };
1594 let ok = |c: &u8| match kind {
1595 2 => c.is_ascii_hexdigit(),
1596 1 => c.is_ascii_digit(),
1597 _ => c.is_ascii_alphanumeric(),
1598 };
1599 let n = b[s..].iter().take(32).take_while(|c| ok(c)).count();
1600 (n > 0 && b.get(s + n) == Some(&b';')).then_some((s + n + 1 - i, SY_ESCAPE))
1601}
1602
1603static HTML_TAG_RULES: [Rule; 5] = [
1604 Rule::Custom(html_tag_name),
1605 Rule::Str { open: "\"", close: "\"", escape: false, multi: true, kind: SY_STRING },
1606 Rule::Str { open: "'", close: "'", escape: false, multi: true, kind: SY_STRING },
1607 Rule::Custom(html_bare_value),
1608 Rule::Custom(html_attr),
1609];
1610
1611static HTML_TAG: Grammar = Grammar {
1613 name: "HTML tag",
1614 exts: &[],
1615 rules: &HTML_TAG_RULES,
1616 ident: "-:.",
1617 calls: false,
1618 macros: false,
1619 caps_types: false,
1620 comment: "",
1621 openers: "",
1622 closers: "",
1623 pairs: "",
1624};
1625
1626static HTML_RULES: [Rule; 8] = [
1627 Rule::Block { open: "<!--", close: "-->", kind: SY_COMMENT, nested: false, bol: false },
1628 Rule::Block { open: "<![CDATA[", close: "]]>", kind: SY_CODE, nested: false, bol: false },
1629 Rule::Custom(html_decl),
1630 Rule::Element { name: "script", tag: &HTML_TAG, inner: &JAVASCRIPT },
1631 Rule::Element { name: "style", tag: &HTML_TAG, inner: &CSS },
1632 Rule::Custom(html_lone_lt),
1633 Rule::Nest { open: "<", close: ">", kind: SY_PUNCT, inner: &HTML_TAG },
1634 Rule::Custom(html_entity),
1635];
1636
1637pub static HTML: Grammar = Grammar {
1642 name: "HTML",
1643 exts: &["html", "htm", "xhtml", "xml", "svg"],
1644 rules: &HTML_RULES,
1645 ident: "",
1646 calls: false,
1647 macros: false,
1648 caps_types: false,
1649 comment: "",
1650 openers: "",
1651 closers: "",
1652 pairs: "<>",
1653};
1654
1655fn wat_id(t: &str, i: usize, _: bool) -> Option<(usize, u8)> {
1657 let b = t.as_bytes();
1658 if b[i] != b'$' {
1659 return None;
1660 }
1661 let n = b[i + 1..].iter().take_while(|&&c| c.is_ascii_alphanumeric() || b"!#$%&'*+-./:<=>?@\\^_`|~".contains(&c)).count();
1662 (n > 0).then_some((n + 1, SY_META))
1663}
1664
1665fn wat_instr(t: &str, i: usize, _: bool) -> Option<(usize, u8)> {
1667 let b = t.as_bytes();
1668 if !b[i].is_ascii_lowercase() || t[..i].chars().next_back().is_some_and(|c| c.is_alphanumeric() || c == '_' || c == '.' || c == '$') {
1669 return None;
1670 }
1671 let n = b[i..].iter().take_while(|&&c| c.is_ascii_alphanumeric() || c == b'_' || c == b'.').count();
1672 t[i..i + n].contains('.').then_some((n, SY_FUNC))
1673}
1674
1675const WAT_KEYWORDS: &[&str] = &[
1676 "module", "func", "param", "result", "local", "global", "memory", "table", "type", "import", "export", "start", "elem", "data", "offset",
1677 "align", "mut", "block", "loop", "if", "then", "else", "end", "declare", "item", "rec", "sub", "final", "struct", "array", "field", "tag",
1678 "shared", "binary", "quote",
1679];
1680const WAT_INSTR: &[&str] = &[
1681 "unreachable", "nop", "br", "br_if", "br_table", "br_on_null", "br_on_non_null", "br_on_cast", "br_on_cast_fail", "return", "call",
1682 "call_indirect", "call_ref", "return_call", "return_call_indirect", "return_call_ref", "drop", "select", "try", "try_table", "catch",
1683 "catch_ref", "catch_all", "catch_all_ref", "throw", "throw_ref", "rethrow", "delegate",
1684];
1685const WAT_TYPES: &[&str] = &[
1686 "i32", "i64", "f32", "f64", "v128", "i8", "i16", "funcref", "externref", "anyref", "eqref", "i31ref", "structref", "arrayref", "nullref",
1687 "nullfuncref", "nullexternref", "exnref", "ref", "null", "extern", "any", "eq", "i31", "none", "noextern", "nofunc",
1688];
1689
1690static WAT_RULES: [Rule; 11] = [
1691 Rule::LineComment(";;"),
1692 Rule::Block { open: "(;", close: ";)", kind: SY_COMMENT, nested: true, bol: false },
1693 Rule::Str { open: "\"", close: "\"", escape: true, multi: false, kind: SY_STRING },
1694 Rule::Custom(wat_id),
1695 Rule::Custom(wat_instr),
1696 Rule::Words(WAT_KEYWORDS, SY_KEYWORD),
1697 Rule::Words(WAT_INSTR, SY_FUNC),
1698 Rule::Words(WAT_TYPES, SY_TYPE),
1699 Rule::Words(&["nan", "inf"], SY_CONST),
1700 Rule::Number,
1701 Rule::Punct("()="),
1702];
1703
1704pub static WAT: Grammar = Grammar {
1708 name: "WAT",
1709 exts: &["wat", "wast"],
1710 rules: &WAT_RULES,
1711 ident: "_",
1712 calls: false,
1713 macros: false,
1714 caps_types: false,
1715 comment: ";;",
1716 openers: "(",
1717 closers: ")",
1718 pairs: "()",
1719};
1720
1721static PLAIN_RULES: [Rule; 0] = [];
1722
1723pub static PLAIN: Grammar = Grammar {
1725 name: "Text",
1726 exts: &["txt", "text", "log"],
1727 rules: &PLAIN_RULES,
1728 ident: "",
1729 calls: false,
1730 macros: false,
1731 caps_types: false,
1732 comment: "",
1733 openers: "",
1734 closers: "",
1735 pairs: "()[]{}",
1736};
1737
1738#[derive(Clone, Debug, PartialEq)]
1742pub struct CodeTheme {
1743 pub bg: u32,
1744 pub fg: u32,
1745 pub gutter_bg: u32,
1746 pub gutter_fg: u32,
1747 pub gutter_cur: u32,
1749 pub line_hl: u32,
1751 pub sel: u32,
1753 pub sel_inactive: u32,
1754 pub sel_text: u32,
1756 pub bracket: u32,
1758 pub find: u32,
1760 pub find_cur: u32,
1761 pub find_frame: bool,
1763 pub caret: u32,
1764 pub tokens: [u32; SY_COUNT],
1765 pub bold: u32,
1766 pub italic: u32,
1767 pub diag: [u32; 3],
1769 pub breakpoint: u32,
1771 pub exec_line: u32,
1774 pub exec_mark: u32,
1775}
1776
1777const fn argb(rgb: u32) -> u32 {
1778 0xFF00_0000 | rgb
1779}
1780
1781impl CodeTheme {
1782 pub fn light() -> CodeTheme {
1784 let fg = argb(0x1F2328);
1785 CodeTheme {
1786 bg: argb(0xFFFFFF),
1787 fg,
1788 gutter_bg: argb(0xF6F8FA),
1789 gutter_fg: argb(0x8C959F),
1790 gutter_cur: fg,
1791 line_hl: argb(0xF1F4F8),
1792 sel: argb(0xB6D7FF),
1793 sel_inactive: argb(0xE1E6EB),
1794 sel_text: 0,
1795 bracket: argb(0x8C959F),
1796 find: argb(0xFFE58A),
1797 find_cur: argb(0xFFA94D),
1798 find_frame: false,
1799 caret: fg,
1800 tokens: [
1801 fg,
1802 argb(0xCF222E),
1803 argb(0x953800),
1804 argb(0x8250DF),
1805 argb(0x0A3069),
1806 argb(0x0550AE),
1807 argb(0x6E7781),
1808 argb(0x0550AE),
1809 argb(0x57606A),
1810 argb(0x116329),
1811 argb(0x0550AE),
1812 argb(0x0550AE),
1813 fg,
1814 fg,
1815 argb(0x0969DA),
1816 argb(0x0A3069),
1817 argb(0x116329),
1818 ],
1819 bold: 1 << SY_HEADING | 1 << SY_STRONG,
1820 italic: 1 << SY_COMMENT | 1 << SY_EMPH,
1821 diag: [argb(0xCF222E), argb(0x9A6700), argb(0x0969DA)],
1822 breakpoint: argb(0xD1242F),
1823 exec_line: argb(0xFFF1BF),
1824 exec_mark: argb(0x9A6700),
1825 }
1826 }
1827
1828 pub fn dark() -> CodeTheme {
1830 let fg = argb(0xE6EDF3);
1831 CodeTheme {
1832 bg: argb(0x0D1117),
1833 fg,
1834 gutter_bg: argb(0x0D1117),
1835 gutter_fg: argb(0x6E7681),
1836 gutter_cur: fg,
1837 line_hl: argb(0x161B22),
1838 sel: argb(0x264F78),
1839 sel_inactive: argb(0x30363D),
1840 sel_text: 0,
1841 bracket: argb(0x8B949E),
1842 find: argb(0x5C4A14),
1843 find_cur: argb(0x9E6A03),
1844 find_frame: false,
1845 caret: fg,
1846 tokens: [
1847 fg,
1848 argb(0xFF7B72),
1849 argb(0xFFA657),
1850 argb(0xD2A8FF),
1851 argb(0xA5D6FF),
1852 argb(0x79C0FF),
1853 argb(0x8B949E),
1854 argb(0x79C0FF),
1855 argb(0xC9D1D9),
1856 argb(0x7EE787),
1857 argb(0x79C0FF),
1858 argb(0x79C0FF),
1859 fg,
1860 fg,
1861 argb(0x58A6FF),
1862 argb(0xA5D6FF),
1863 argb(0x7EE787),
1864 ],
1865 bold: 1 << SY_HEADING | 1 << SY_STRONG,
1866 italic: 1 << SY_COMMENT | 1 << SY_EMPH,
1867 diag: [argb(0xF85149), argb(0xD29922), argb(0x58A6FF)],
1868 breakpoint: argb(0xF85149),
1869 exec_line: argb(0x3B3520),
1870 exec_mark: argb(0xE3B341),
1871 }
1872 }
1873
1874 pub fn high_contrast(c: &plat::HcColors) -> CodeTheme {
1878 let (bg, fg, hi, link, dis) = (argb(c.bg), argb(c.text), argb(c.highlight), argb(c.link), argb(c.disabled));
1879 CodeTheme {
1880 bg,
1881 fg,
1882 gutter_bg: bg,
1883 gutter_fg: fg,
1884 gutter_cur: link,
1885 line_hl: bg,
1886 sel: hi,
1887 sel_inactive: dis,
1888 sel_text: argb(c.highlight_text),
1889 bracket: link,
1890 find: link,
1891 find_cur: hi,
1892 find_frame: true,
1893 caret: fg,
1894 tokens: [fg, link, link, fg, hi, hi, dis, hi, fg, link, link, link, fg, fg, link, hi, link],
1895 bold: 1 << SY_KEYWORD | 1 << SY_HEADING | 1 << SY_STRONG | 1 << SY_FUNC,
1896 italic: 1 << SY_COMMENT | 1 << SY_EMPH,
1897 diag: [fg, link, hi],
1898 breakpoint: link,
1899 exec_line: bg,
1900 exec_mark: hi,
1901 }
1902 }
1903}
1904
1905impl crate::Ui {
1906 pub fn code_theme(&self) -> CodeTheme {
1910 use crate::consts::*;
1911 use crate::theme::color_mix;
1912 if self.high_contrast {
1913 let c = self.hc_sys.unwrap_or(if self.theme_dark { plat::HcColors::BLACK } else { plat::HcColors::WHITE });
1914 return CodeTheme::high_contrast(&c);
1915 }
1916 let mut t = if self.theme_dark { CodeTheme::dark() } else { CodeTheme::light() };
1917 let over = |bg: u32, c: u32| -> u32 {
1918 let a = (c >> 24) as i32;
1919 color_mix(bg | 0xFF00_0000, c | 0xFF00_0000, a + (a >> 7))
1920 };
1921 let bg = over(self.color(T_BG), self.color(T_INPUT_ACTIVE));
1922 let fg = over(bg, self.color(T_TEXT));
1923 t.bg = bg;
1924 t.fg = fg;
1925 t.tokens[SY_TEXT as usize] = fg;
1926 t.tokens[SY_EMPH as usize] = fg;
1927 t.tokens[SY_STRONG as usize] = fg;
1928 t.caret = fg;
1929 t.gutter_bg = color_mix(bg, fg, 10);
1930 t.gutter_fg = over(t.gutter_bg, self.color(T_TEXT3));
1931 t.gutter_cur = fg;
1932 t.line_hl = color_mix(bg, fg, if self.theme_dark { 16 } else { 10 });
1933 t.sel = color_mix(bg, self.color(T_ACCENT) | 0xFF00_0000, if self.theme_dark { 110 } else { 80 });
1934 t.sel_inactive = color_mix(bg, fg, if self.theme_dark { 40 } else { 30 });
1935 t.diag = [over(bg, self.color(T_CRIT)), over(bg, self.color(T_WARN)), over(bg, self.color(T_INFO))];
1936 t.breakpoint = t.diag[0];
1937 t.exec_mark = t.diag[1];
1938 t.exec_line = color_mix(bg, t.diag[1], if self.theme_dark { 48 } else { 40 });
1939 t
1940 }
1941}