Skip to main content

ui/
highlight.rs

1// highlight -- syntax highlighting for ui.code_editor: a small engine of token rules, the
2// built-in grammars (Rust, JSON, INI, TOML, a Markdown subset, JFS: the scripts of
3// JFScript; JavaScript, CSS, HTML with its scripts and styles, WAT: the text format of
4// WebAssembly) and the colours of the tokens
5//
6//   Highlighter::rust() / json() / ini() / toml() / markdown() / jfs() / plain()
7//   Highlighter::javascript() / css() / html() / wat()   (E10: view-source, developer tools)
8//   Highlighter::for_name(s)            by name or file extension ("rs", "json", "ini", "cfg",
9//                                       "toml", "md", "jfs", "js", "css", "html", "xml", "svg",
10//                                       "wat", a path); plain() for anything else
11//   Highlighter::new(&GRAMMAR)          an app's own grammar (a static Grammar of Rules)
12//   h.line(text, state, &mut tokens) -> state   the tokens of one line (SY_* kinds, byte
13//                                       ranges; the plain text between them is SY_TEXT) from
14//                                       the state the line before ended in; 0 = nothing open
15//   h.tokens(text) -> Vec<Vec<Token>>   every line of a text (tests)
16//   CodeTheme::light() / dark() / high_contrast(&HcColors), ui.code_theme()   the colours of
17//                                       the tokens and the editor (gutter, current line,
18//                                       selection, bracket match, find matches, diagnostics,
19//                                       breakpoints, a debugger's current line)
20//
21// A grammar is a list of rules tried in order at every position: line comments, blocks (from
22// open to close, nested or not, optionally only at a line start: Markdown fences), strings
23// (escapes, multi-line or not), strings with code inside (Interp: the ${..} of JFS, whose
24// code the same rules highlight), Rust raw strings, word lists, numbers, punctuation and
25// functions of its own (Custom: JSON keys, Rust char literals / lifetimes / attributes, INI
26// sections and keys, Markdown headings, lists, emphasis and links), parts with rules of their
27// own (Nest: HTML's tags, JavaScript's template strings and their ${..}) and elements whose
28// content is another language (Element: HTML's <script> and <style>).  Where no rule matches,
29// an identifier is read whole: before '(' a function (calls), before '!' a macro (macros),
30// capitalised a type (caps_types).  A construct still open at a line end is the line's end
31// state: rule index + 1, the nesting depth (bits 8..15) and a Rust raw string's '#' count
32// (bits 16..31); inside a Nest or an Element the inner grammar's state in bits 8..31 (so
33// three levels: HTML, a script's template string, its ${..}); the editor keeps a state per
34// line and highlights only the visible lines.
35
36use alloc::vec::Vec;
37
38/// token kinds: plain text
39pub const SY_TEXT: u8 = 0;
40/// token kinds: keywords
41pub const SY_KEYWORD: u8 = 1;
42/// types (lists, capitalised names)
43pub const SY_TYPE: u8 = 2;
44/// functions, macros
45pub const SY_FUNC: u8 = 3;
46/// strings, character literals
47pub const SY_STRING: u8 = 4;
48/// numbers, dates
49pub const SY_NUMBER: u8 = 5;
50/// comments, Markdown quotations
51pub const SY_COMMENT: u8 = 6;
52/// true / false / null, enum constants
53pub const SY_CONST: u8 = 7;
54/// operators, brackets, separators
55pub const SY_PUNCT: u8 = 8;
56/// attributes, lifetimes, preprocessor lines
57pub const SY_META: u8 = 9;
58/// JSON / INI / TOML keys
59pub const SY_KEY: u8 = 10;
60/// Markdown headings, INI / TOML sections
61pub const SY_HEADING: u8 = 11;
62/// Markdown *emphasis*
63pub const SY_EMPH: u8 = 12;
64/// Markdown **strong**
65pub const SY_STRONG: u8 = 13;
66/// links, URLs
67pub const SY_LINK: u8 = 14;
68/// Markdown code spans and blocks
69pub const SY_CODE: u8 = 15;
70/// escape sequences in strings
71pub const SY_ESCAPE: u8 = 16;
72/// the number of kinds
73pub const SY_COUNT: usize = 17;
74
75/// a highlighted part of a line: bytes start .. end
76#[derive(Clone, Copy, Debug, PartialEq, Eq)]
77pub struct Token {
78    pub start: usize,
79    pub end: usize,
80    pub kind: u8,
81}
82
83/// a rule of a grammar (see the module header)
84#[non_exhaustive]
85pub enum Rule {
86    /// from the text to the end of the line: a comment
87    LineComment(&'static str),
88    /// from open to close (nested: opens inside count; bol: open and close only at the start
89    /// of a line, after blanks -- the whole line belongs to it); may span lines
90    Block { open: &'static str, close: &'static str, kind: u8, nested: bool, bol: bool },
91    /// a string from open to close; escape: a backslash takes the next character (SY_ESCAPE);
92    /// multi: may span lines
93    Str { open: &'static str, close: &'static str, escape: bool, multi: bool, kind: u8 },
94    /// Rust raw strings: r"..", r#".."#, br".." (may span lines)
95    RawStr,
96    /// a string with code inside (interpolation), on one line: from open to close, a backslash
97    /// takes the next character (\u{..} its hex digits and brace; SY_ESCAPE); from `start`
98    /// (ending with '{') to the '}' that closes it (braces inside count) the grammar's rules
99    /// highlight code, the two delimiters SY_META
100    Interp { open: &'static str, close: &'static str, start: &'static str },
101    /// identifiers of a list
102    Words(&'static [&'static str], u8),
103    /// number literals: 12, 0x1F, 0b101, 0o17, 1_000, 1.5e-3, suffixes (u8, f32)
104    Number,
105    /// a function of its own: (line, position, at the line start after blanks) -> (bytes, kind)
106    Custom(fn(&str, usize, bool) -> Option<(usize, u8)>),
107    /// single characters
108    Punct(&'static str),
109    /// a part with rules of its own: from open to close (may span lines), the two delimiters
110    /// `kind`, the text between them highlighted by `inner`'s rules (HTML's tags, the ${..} of
111    /// JavaScript's template strings); close counts only outside inner's strings and comments,
112    /// and with close "}" the braces inside count
113    Nest { open: &'static str, close: &'static str, kind: u8, inner: &'static Grammar },
114    /// an element whose content is another language (HTML's script and style): the start tag
115    /// from '<' and `name` (ASCII case-insensitive, then a blank, '/' or '>') to its '>' on the
116    /// same line ('<' and '>' SY_PUNCT, the rest by `tag`'s rules), then the content up to
117    /// "</" and `name` (case-insensitive, wherever it stands) by `inner`'s rules; may span
118    /// lines; the end tag is the grammar's own again
119    Element { name: &'static str, tag: &'static Grammar, inner: &'static Grammar },
120}
121
122/// a language (see the module header)
123pub struct Grammar {
124    pub name: &'static str,
125    /// file extensions (lower case, without the dot)
126    pub exts: &'static [&'static str],
127    pub rules: &'static [Rule],
128    /// characters inside identifiers besides letters, digits and '_'
129    pub ident: &'static str,
130    /// an identifier before '(' is a function
131    pub calls: bool,
132    /// an identifier before '!' is a macro (Rust)
133    pub macros: bool,
134    /// capitalised identifiers are types
135    pub caps_types: bool,
136    /// the line comment of Ctrl+/ ("": none)
137    pub comment: &'static str,
138    /// Enter after one of these indents one level more
139    pub openers: &'static str,
140    /// one of these typed on a blank line goes one level out
141    pub closers: &'static str,
142    /// the bracket pairs the editor matches: "()[]{}"
143    pub pairs: &'static str,
144}
145
146/// the highlighter of a grammar (see the module header)
147#[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
169// a Nest / Element open at the line end: its rule and the inner grammar's state above it
170// (bits 8..31; what does not fit is lost)
171fn enc_in(rule: usize, inner: u32) -> u32 {
172    (rule as u32 + 1) & 0xFF | inner << 8
173}
174
175// the place of "</" + name (ASCII case-insensitive) from i on
176fn 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
189// an element's start tag at i ("<name" then a blank, '/' or '>'): the place of its '>'
190fn 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    /// an app's own grammar
215    pub const fn new(g: &'static Grammar) -> Highlighter {
216        Highlighter { g }
217    }
218    /// plain text: no tokens
219    pub const fn plain() -> Highlighter {
220        Highlighter { g: &PLAIN }
221    }
222    /// Rust
223    pub const fn rust() -> Highlighter {
224        Highlighter { g: &RUST }
225    }
226    /// JSON (and JSONC comments)
227    pub const fn json() -> Highlighter {
228        Highlighter { g: &JSON }
229    }
230    /// INI files
231    pub const fn ini() -> Highlighter {
232        Highlighter { g: &INI }
233    }
234    /// TOML
235    pub const fn toml() -> Highlighter {
236        Highlighter { g: &TOML }
237    }
238    /// a Markdown subset
239    pub const fn markdown() -> Highlighter {
240        Highlighter { g: &MARKDOWN }
241    }
242    /// the scripting language of JFScript (.jfs, JFNetworker's scripts)
243    pub const fn jfs() -> Highlighter {
244        Highlighter { g: &JFS }
245    }
246    /// JavaScript (template strings over lines with their ${..}, regular expressions)
247    pub const fn javascript() -> Highlighter {
248        Highlighter { g: &JAVASCRIPT }
249    }
250    /// CSS (selectors, at-rules, properties, values with units and colours, nested rules)
251    pub const fn css() -> Highlighter {
252        Highlighter { g: &CSS }
253    }
254    /// HTML (also XML and SVG): tags, attributes, comments, entities, doctype; the content of
255    /// <script> as JavaScript and of <style> as CSS
256    pub const fn html() -> Highlighter {
257        Highlighter { g: &HTML }
258    }
259    /// WAT, the text format of WebAssembly (modules, instructions, $names, types)
260    pub const fn wat() -> Highlighter {
261        Highlighter { g: &WAT }
262    }
263
264    /// by name or extension, also of a path ("rs", "Rust", "x/y.toml"); plain() for others
265    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    /// the grammar (comment prefix, openers, closers, bracket pairs)
278    pub fn grammar(&self) -> &'static Grammar {
279        self.g
280    }
281
282    /// the grammar's name ("Rust", "JSON" ...)
283    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    // the identifier starting at i (an index past its end) or None
292    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    /// the tokens of one line from the state of the line before -> the state at its end
312    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    // (code_long.rs) the tokens of line t from `from` on -- 0, or a place outside every
318    // construct that a scan of the line from `state` passed -- appended to out; at(i, out) sees
319    // each place outside every construct from `from` on (and the tokens so far), true stops
320    // there -> (where the scan stopped, the state at the line end; None: at stopped it)
321    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    // the tokens from i on (code: inside an interpolation, up to the '}' that closes it; else
335    // at(i) sees each place between constructs, true stops there) -> (where it stopped, the
336    // state at the line end: 0 = nothing open; None: at stopped it)
337    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            // no rule: an identifier whole, else one character
483            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    // a string with interpolations from its open quote at i (open: its length) -> (its end,
516    // the state at the line end: a construct inside an interpolation stays open)
517    #[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    // continue a construct open at the line start -> (where it ends, the state: 0 closed)
564    fn resume(&self, t: &str, state: u32, bol: usize, out: &mut Vec<Token>) -> (usize, u32) {
565        let n = t.len();
566        // (a low byte 0: no construct, e.g. a state whose bits were shifted out)
567        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    // inside a Nest (self: its inner grammar): the tokens from i (from the state st open at the
612    // line start when st != 0) up to `close` outside every construct of the inner grammar
613    // (close "}": the braces inside count) -> Ok(where close starts) / Err(the state at the end)
614    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        // close "}": the braces inside count, unless a rule of the inner grammar nests them
624        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    // inside an Element (self: the language of its content): the tokens from i (from the state
645    // st open at the line start when st != 0) up to "</name" -> Ok(where it starts; the content's
646    // state ends there whatever it was) / Err(the state at the line end)
647    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    /// the tokens of every line of a text (tests)
663    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
675// from i inside a block (depth open blocks): the end and the depth left (0: closed)
676fn scan_block(t: &str, i: usize, open: &str, close: &str, nested: bool, depth: u32) -> (usize, u32) {
677    // an open block has depth 1 at least: a depth the 32-bit line state lost (a block more than
678    // three constructs deep, whose bits were shifted out) counts as 1, d -= 1 must not pass 0
679    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
700// a string from start (its content from i): tokens with escapes -> (end, closed)
701fn 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
735// a Rust raw string at i: (the index after its opening quote, the '#' count)
736fn 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
754// the end of a raw string's closing quote and hashes from i (None: not on this line)
755fn 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
767// the end of the number literal at i
768fn 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            // an exponent's sign
779            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
793// ------------------------------------------------------------------ custom rules
794
795// Rust: 'a' / '\n' / '\u{..}' char literals, else a lifetime 'a
796fn 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
823// Rust: #[...] / #![...] attributes (brackets counted, to the line end when unclosed)
824fn 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
851// JSON: a string; followed by ':' a key
852fn 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
872// INI / TOML: [section] / [[table]] at a line start
873fn 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
887// INI / TOML: the key before '=' (INI also ':') at a line start
888fn 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
905// a URL: http(s)://.. to a blank or a closing bracket / quote
906fn 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
914// Markdown: '#'.. heading lines
915fn 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
927// Markdown: '>' quotation lines
928fn 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
932// Markdown: ---, *** and ___ rules
933fn 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
946// Markdown: list markers "- ", "* ", "+ ", "1. ", "1) "
947fn 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
962// Markdown: **strong** / __strong__ and *emphasis* / _emphasis_ (not inside words)
963fn 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
993// Markdown: [text](url), ![alt](url), <url>, bare URLs
994fn 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
1009// Markdown: \* and other backslash escapes
1010fn 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
1015// TOML: dates and times (1979-05-27, 07:32:00, 1979-05-27T07:32:00Z) as one number
1016fn 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
1030// ------------------------------------------------------------------ the grammars
1031
1032const 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
1057/// Rust
1058pub 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
1081/// JSON (comments of JSONC too)
1082pub 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
1113/// INI files (; and # comments at a line start, [sections], key = value, key: value)
1114pub 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
1143/// TOML
1144pub 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
1172/// a Markdown subset: headings, quotations, rules, lists, fenced code, code spans, emphasis,
1173/// strong, links, escapes, HTML comments
1174pub 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
1188// the lexical rules of JFScript's script/lex.rs: keywords, true / false / nil, one-line strings
1189// with escapes and ${..} (code inside, nested strings too), numbers with _, 0x, 0b, a
1190// fraction and an exponent, // and /* */ comments (not nested)
1191const 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
1203/// the scripting language of JFScript (.jfs): calls before '(' are functions, the
1204/// delimiters of ${..} in strings SY_META
1205pub 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
1219// ------------------------------------------------------------------ the web's languages (E10)
1220
1221// JavaScript: a regular expression literal where an expression may start (the line start, an
1222// operator, an opening bracket, a keyword like return), its class [..] and flags; on one line
1223fn 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
1267// the length of an identifier of JavaScript at i (letters, digits, '_', '$'; not a digit first)
1268fn 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
1279// JavaScript: a member after '.' (not "...") is no keyword: before '(' a function, else plain
1280// text; identifiers that start with '$' the same way
1281fn 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
1295// a template string's text: up to '`', a backslash or "${"
1296fn 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
1305// a template string's escape: the backslash and the next character
1306fn 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
1323// the inside of a template string `..` (Nest of JAVASCRIPT)
1324static 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
1352/// JavaScript: keywords, constants, strings, template strings over lines (their ${..} with
1353/// SY_META delimiters, code inside), regular expressions (SY_STRING), numbers (also 1n, 0x..,
1354/// 1_000), // and /* */ comments; calls before '(' are functions, capitalised names types, a
1355/// member after '.' never a keyword
1356pub 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
1370// CSS: the text after i up to the next '{', ';' or '}' ends with '{': a selector (of a nested
1371// rule), not a declaration
1372fn 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
1380// CSS: a property's name (also --custom) after a line start, '{' or ';' and before ':'
1381fn 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
1397// CSS: at-rules (@media, @import ..)
1398fn 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
1407// CSS: selectors -- .class (SY_TYPE), #id (SY_CONST), :pseudo and ::pseudo (SY_META); in a
1408// declaration block a pseudo only in the selector of a nested rule, and #hex a colour
1409fn 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
1441// CSS: numbers with their unit or '%' (also -1.5em, .5s); !important
1442fn 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
1502// the inside of a CSS block { .. }: declarations and nested rules (Nest of CSS)
1503static 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
1517/// CSS: selectors (.class SY_TYPE, #id SY_CONST, :pseudo SY_META), at-rules (SY_KEYWORD),
1518/// blocks over lines with properties (SY_KEY), numbers with units, #colours (SY_NUMBER),
1519/// functions, !important, strings, /* */ comments; nested rules
1520pub 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
1538// HTML: a tag's name right after '<' or "</"
1539fn 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
1547// HTML: an attribute's value without quotes after '='
1548fn 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
1556// HTML: an attribute's name
1557fn 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
1563// HTML: <!DOCTYPE ..>, <?xml ..?> to their '>' (on one line)
1564fn 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
1572// HTML: a '<' that opens no tag (a blank or a digit after it) is text
1573fn 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
1583// HTML: character references &amp; &#169; &#xA9;
1584fn 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
1611// the inside of a tag < .. >: its name, attributes and values (Nest of HTML)
1612static 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
1637/// HTML (XML and SVG too): tags over lines (names SY_KEYWORD, attributes SY_KEY, values
1638/// SY_STRING, '<' and '>' SY_PUNCT), comments, CDATA (SY_CODE), <!DOCTYPE> and <?xml?>
1639/// (SY_META), character references (SY_ESCAPE); the content of <script> highlighted as
1640/// JavaScript and of <style> as CSS up to its end tag
1641pub 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
1655// WAT: $names (identifiers of functions, locals, labels, types)
1656fn 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
1665// WAT: an instruction with a '.' (i32.add, local.get, memory.grow, v128.load8x8_s)
1666fn 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
1704/// WAT, the text format of WebAssembly: module fields and block keywords (SY_KEYWORD),
1705/// instructions (SY_FUNC: i32.add, local.get, call ...), value and reference types
1706/// (SY_TYPE), $names (SY_META), numbers, strings, ;; and nested (; ;) comments
1707pub 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
1723/// plain text: no tokens
1724pub 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// ------------------------------------------------------------------ colours
1739
1740/// the colours of a code editor (0xAARRGGBB); bold / italic: a bit per SY_* kind
1741#[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    /// the current line's number
1748    pub gutter_cur: u32,
1749    /// the current line's background
1750    pub line_hl: u32,
1751    /// the selection (focused / not focused)
1752    pub sel: u32,
1753    pub sel_inactive: u32,
1754    /// the selected text (0: the tokens keep their colours)
1755    pub sel_text: u32,
1756    /// the frame around a matching bracket pair
1757    pub bracket: u32,
1758    /// the find matches and the current one
1759    pub find: u32,
1760    pub find_cur: u32,
1761    /// find matches framed instead of filled (high contrast)
1762    pub find_frame: bool,
1763    pub caret: u32,
1764    pub tokens: [u32; SY_COUNT],
1765    pub bold: u32,
1766    pub italic: u32,
1767    /// the wavy underlines and gutter markers of errors, warnings and infos (DIAG_* order)
1768    pub diag: [u32; 3],
1769    /// breakpoints in the gutter
1770    pub breakpoint: u32,
1771    /// the background of a debugger's current line (the editor's background: a frame in
1772    /// exec_mark instead) and its arrow in the gutter
1773    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    /// on a light background (the colours of GitHub's light theme)
1783    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    /// on a dark background (the colours of GitHub's dark theme)
1829    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    /// the colours of a high-contrast scheme: few colours, keywords and headings bold;
1875    /// diagnostics in the text, link and highlight colours (the gutter markers' shapes tell
1876    /// them apart too)
1877    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    /// the code editors' colours for the UI's theme: dark / light syntax colours on the UI's
1907    /// text box colours (accent selection, diagnostics in its critical / warning / info
1908    /// colours), or the high-contrast scheme's colours
1909    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}