tawasuyu

tawasuyu/llimphi

Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.

Rust 660Markdown 302TOML 157RON 312382 Sutichaykuna

⎇ main widgets/text-editor-core/src/sintaxis.rs Rust · 1892 siq'ikuna · 73.6 KB Ñawpa willay Pi imata llamkarqan ch'uya

chore: refresco desde el monorepo (pin tawasuyu.git v0.3.0) b575c63f1 · Sergio ·

1//! **Coloración por lenguaje, sin gramáticas C.**
2//!
3//! `highlight` tenía tres caminos: tree-sitter (Rust/Python), un tokenizer a
4//! mano (WAT) y `Plain` — o sea, **todo lo demás en gris**. Un árbol de este
5//! workspace abre `.toml`, `.md`, `.json`, `.sh`, `.yaml`, `.html`, `.css`,
6//! `.sql`, `.js`, `.go`… y en el lienzo todos se veían iguales: un bloque de
7//! texto plano.
8//!
9//! Acá viven los que faltaban, en **Rust puro**: nada de gramáticas nuevas ni
10//! de una toolchain C por lenguaje (la feature `treesitter` sigue siendo la que
11//! manda para Rust/Python, y sin ella estos mismos tokenizadores los cubren en
12//! vez de degradar a plano).
13//!
14//! Dos formas, según lo que pida el lenguaje:
15//!
16//! - **[`Sintaxis`] + [`tokenizar`]** — la tabla: palabras clave, tipos,
17//!   constantes, comentarios de línea y de bloque, comillas, anotaciones.
18//!   Cubre toda la familia de llaves (C, Go, JS/TS, Java, Kotlin, Swift, PHP,
19//!   Zig, Nim…) y también las de `#` (shell, Ruby) o `--` (SQL, Lua, Haskell).
20//! - **tokenizadores propios** para los que no son un lenguaje de expresiones:
21//!   Markdown, YAML, TOML/INI, HTML/XML, CSS y JSON (que sí es de llaves, pero
22//!   quiere sus **claves** de otro color que sus cadenas).
23//!
24//! El contrato es el de `highlight`: una `Vec<Span>` por línea que **cubre la
25//! línea entera** (los huecos los rellena `fill_gaps` con `Other`), con las
26//! mismas diez categorías de [`TokenKind`] — no se agregan colores nuevos al
27//! theme, se reparten mejor los que ya hay.
28
29use alloc::string::String;
30use alloc::vec;
31use alloc::vec::Vec;
32
33use crate::highlight::{fill_gaps, iterate_lines, Language, Span, TokenKind};
34
35// =============================================================================
36//  La tabla de un lenguaje de expresiones
37// =============================================================================
38
39/// Lo que hace falta saber de un lenguaje para tokenizarlo por tabla.
40///
41/// No es un parser: no arma árbol ni entiende ámbitos. Es lo que distingue un
42/// comentario de una cadena de un número de una palabra clave — que es el 90 %
43/// de lo que un lector saca del color.
44pub struct Sintaxis {
45    /// Palabras clave (`if`, `func`, `impl`…).
46    pub palabras: &'static [&'static str],
47    /// Tipos primitivos y afines (`i32`, `String`, `bool`…).
48    pub tipos: &'static [&'static str],
49    /// Literales con nombre (`true`, `null`, `nil`…) — van con los números.
50    pub constantes: &'static [&'static str],
51    /// Prefijos de comentario hasta fin de línea (`//`, `#`, `--`, `;`).
52    pub comentario_linea: &'static [&'static str],
53    /// Delimitadores de comentario de bloque, si los hay.
54    pub comentario_bloque: Option<(&'static str, &'static str)>,
55    /// Comillas que abren una cadena.
56    pub comillas: &'static [char],
57    /// Si las cadenas triples (`"""`, `'''`) cruzan renglones.
58    pub triples: bool,
59    /// Palabras clave sin distinguir mayúsculas (SQL).
60    pub ignora_caja: bool,
61    /// Caracteres extra admitidos **dentro** de un identificador (`!` de
62    /// `println!`, `?` de `empty?`).
63    pub ident_extra: &'static [char],
64    /// Caracteres que pueden **abrir** un identificador además de letra y `_`
65    /// (`$` de shell/JS/PHP, `@` de Ruby). Van aparte del cuerpo a propósito:
66    /// con `!` como apertura, el `!` de `if !cond` se comía la palabra.
67    pub inicio_extra: &'static [char],
68    /// Si un identificador que empieza en mayúscula se pinta como tipo.
69    pub tipo_por_mayuscula: bool,
70    /// Prefijos de anotación/decorador (`@` en Java/Python, `#` en Ruby no).
71    pub anotacion: &'static [char],
72    /// Si una línea que empieza con `#` es una directiva de preprocesador (C).
73    pub preprocesador: bool,
74}
75
76impl Sintaxis {
77    const fn base() -> Self {
78        Sintaxis {
79            palabras: &[],
80            tipos: &[],
81            constantes: &[],
82            comentario_linea: &["//"],
83            comentario_bloque: Some(("/*", "*/")),
84            comillas: &['"', '\''],
85            triples: false,
86            ignora_caja: false,
87            ident_extra: &[],
88            inicio_extra: &[],
89            tipo_por_mayuscula: false,
90            anotacion: &[],
91            preprocesador: false,
92        }
93    }
94}
95
96/// Lo que quedó abierto al terminar un renglón.
97#[derive(Clone, Copy, PartialEq, Eq)]
98enum Arrastre {
99    Nada,
100    Bloque,
101    /// Cadena triple abierta con esa comilla.
102    Triple(char),
103}
104
105/// Tokeniza el fuente entero contra una [`Sintaxis`]: un `Vec<Span>` por línea.
106pub fn tokenizar(fuente: &str, s: &Sintaxis) -> Vec<Vec<Span>> {
107    let mut arrastre = Arrastre::Nada;
108    let mut out: Vec<Vec<Span>> = Vec::new();
109    for linea in iterate_lines(fuente) {
110        let ch: Vec<char> = linea.chars().collect();
111        out.push(tokenizar_linea(&ch, s, &mut arrastre));
112    }
113    out
114}
115
116fn tokenizar_linea(ch: &[char], s: &Sintaxis, arrastre: &mut Arrastre) -> Vec<Span> {
117    let len = ch.len();
118    let mut out: Vec<Span> = Vec::new();
119    let mut i = 0usize;
120
121    // Lo que venía abierto del renglón anterior se cierra primero.
122    match *arrastre {
123        Arrastre::Bloque => {
124            let cierre = s.comentario_bloque.map(|(_, c)| c).unwrap_or("*/");
125            match buscar(ch, 0, cierre) {
126                Some(p) => {
127                    let fin = p + cierre.chars().count();
128                    out.push(Span { start_col: 0, end_col: fin, kind: TokenKind::Comment });
129                    *arrastre = Arrastre::Nada;
130                    i = fin;
131                }
132                None => {
133                    return vec![Span { start_col: 0, end_col: len, kind: TokenKind::Comment }];
134                }
135            }
136        }
137        Arrastre::Triple(q) => {
138            let cierre: String = core::iter::repeat(q).take(3).collect();
139            match buscar(ch, 0, &cierre) {
140                Some(p) => {
141                    let fin = p + 3;
142                    out.push(Span { start_col: 0, end_col: fin, kind: TokenKind::String });
143                    *arrastre = Arrastre::Nada;
144                    i = fin;
145                }
146                None => {
147                    return vec![Span { start_col: 0, end_col: len, kind: TokenKind::String }];
148                }
149            }
150        }
151        Arrastre::Nada => {}
152    }
153
154    // Directiva de preprocesador: `#include`, `#define`… La línea entera pasa
155    // por el tokenizador normal; sólo la palabra del `#` cambia de categoría.
156    let mut primera_no_blanca = true;
157
158    while i < len {
159        let c = ch[i];
160
161        if c.is_whitespace() {
162            let ini = i;
163            while i < len && ch[i].is_whitespace() {
164                i += 1;
165            }
166            out.push(Span { start_col: ini, end_col: i, kind: TokenKind::Other });
167            continue;
168        }
169
170        // Comentario de línea: se come lo que queda del renglón.
171        if s.comentario_linea.iter().any(|p| empieza(ch, i, p)) {
172            out.push(Span { start_col: i, end_col: len, kind: TokenKind::Comment });
173            break;
174        }
175
176        // Comentario de bloque.
177        if let Some((abre, cierra)) = s.comentario_bloque {
178            if empieza(ch, i, abre) {
179                let desde = i + abre.chars().count();
180                match buscar(ch, desde, cierra) {
181                    Some(p) => {
182                        let fin = p + cierra.chars().count();
183                        out.push(Span { start_col: i, end_col: fin, kind: TokenKind::Comment });
184                        i = fin;
185                    }
186                    None => {
187                        out.push(Span { start_col: i, end_col: len, kind: TokenKind::Comment });
188                        *arrastre = Arrastre::Bloque;
189                        i = len;
190                    }
191                }
192                continue;
193            }
194        }
195
196        // Cadena (triple primero: `"""` es también `"`).
197        if s.comillas.contains(&c) {
198            if s.triples && i + 2 < len && ch[i + 1] == c && ch[i + 2] == c {
199                let cierre: String = core::iter::repeat(c).take(3).collect();
200                match buscar(ch, i + 3, &cierre) {
201                    Some(p) => {
202                        let fin = p + 3;
203                        out.push(Span { start_col: i, end_col: fin, kind: TokenKind::String });
204                        i = fin;
205                    }
206                    None => {
207                        out.push(Span { start_col: i, end_col: len, kind: TokenKind::String });
208                        *arrastre = Arrastre::Triple(c);
209                        i = len;
210                    }
211                }
212                continue;
213            }
214            let ini = i;
215            i = fin_de_cadena(ch, i, c);
216            out.push(Span { start_col: ini, end_col: i, kind: TokenKind::String });
217            continue;
218        }
219
220        // Número.
221        if c.is_ascii_digit()
222            || (c == '.' && i + 1 < len && ch[i + 1].is_ascii_digit() && !es_ident(ch, i))
223        {
224            let ini = i;
225            while i < len
226                && (ch[i].is_ascii_alphanumeric() || ch[i] == '.' || ch[i] == '_')
227                // `1..10` es un rango, no un número con dos puntos.
228                && !(ch[i] == '.' && i + 1 < len && ch[i + 1] == '.')
229            {
230                i += 1;
231            }
232            out.push(Span { start_col: ini, end_col: i, kind: TokenKind::Number });
233            continue;
234        }
235
236        // Anotación / decorador: `@Override`, `@dataclass`.
237        if s.anotacion.contains(&c) && i + 1 < len && inicio_ident(ch[i + 1], s) {
238            let ini = i;
239            i += 1;
240            while i < len && cuerpo_ident(ch[i], s) {
241                i += 1;
242            }
243            out.push(Span { start_col: ini, end_col: i, kind: TokenKind::Function });
244            continue;
245        }
246
247        // Directiva de preprocesador.
248        if s.preprocesador && primera_no_blanca && c == '#' {
249            let ini = i;
250            i += 1;
251            while i < len && ch[i].is_ascii_alphabetic() {
252                i += 1;
253            }
254            out.push(Span { start_col: ini, end_col: i, kind: TokenKind::Keyword });
255            primera_no_blanca = false;
256            continue;
257        }
258
259        // Palabra.
260        if inicio_ident(c, s) {
261            let ini = i;
262            while i < len && cuerpo_ident(ch[i], s) {
263                i += 1;
264            }
265            let palabra: String = ch[ini..i].iter().collect();
266            let kind = clasificar(&palabra, ch, i, s);
267            out.push(Span { start_col: ini, end_col: i, kind });
268            primera_no_blanca = false;
269            continue;
270        }
271
272        primera_no_blanca = false;
273        let kind = if "()[]{},;".contains(c) {
274            TokenKind::Punctuation
275        } else {
276            TokenKind::Operator
277        };
278        out.push(Span { start_col: i, end_col: i + 1, kind });
279        i += 1;
280    }
281
282    fill_gaps(out, len)
283}
284
285/// Qué es una palabra: clave, tipo, constante, llamada o identificador.
286fn clasificar(palabra: &str, ch: &[char], fin: usize, s: &Sintaxis) -> TokenKind {
287    let contiene = |tabla: &[&str]| -> bool {
288        if s.ignora_caja {
289            tabla.iter().any(|k| k.eq_ignore_ascii_case(palabra))
290        } else {
291            tabla.contains(&palabra)
292        }
293    };
294    if contiene(s.palabras) {
295        return TokenKind::Keyword;
296    }
297    if contiene(s.tipos) {
298        return TokenKind::Type;
299    }
300    if contiene(s.constantes) {
301        return TokenKind::Number;
302    }
303    // `nombre(` es una llamada (o una definición): el paréntesis lo dice sin
304    // necesidad de saber en qué ámbito estamos.
305    let mut j = fin;
306    while j < ch.len() && ch[j] == ' ' {
307        j += 1;
308    }
309    if j < ch.len() && ch[j] == '(' {
310        return TokenKind::Function;
311    }
312    if s.tipo_por_mayuscula && palabra.chars().next().is_some_and(|c| c.is_uppercase()) {
313        return TokenKind::Type;
314    }
315    TokenKind::Identifier
316}
317
318fn inicio_ident(c: char, s: &Sintaxis) -> bool {
319    c.is_alphabetic() || c == '_' || s.inicio_extra.contains(&c)
320}
321
322fn cuerpo_ident(c: char, s: &Sintaxis) -> bool {
323    c.is_alphanumeric() || c == '_' || s.ident_extra.contains(&c) || s.inicio_extra.contains(&c)
324}
325
326/// `true` si la posición `i` está dentro de un identificador (para no leer el
327/// `.` de `objeto.5` como número).
328fn es_ident(ch: &[char], i: usize) -> bool {
329    i > 0 && (ch[i - 1].is_alphanumeric() || ch[i - 1] == '_')
330}
331
332/// Fin (exclusivo) de una cadena abierta en `i` con la comilla `q`. Si no
333/// cierra en el renglón, termina en el renglón: una comilla suelta no debe
334/// teñir de verde el resto del archivo.
335fn fin_de_cadena(ch: &[char], i: usize, q: char) -> usize {
336    let len = ch.len();
337    let mut j = i + 1;
338    while j < len {
339        if ch[j] == '\\' && j + 1 < len {
340            j += 2;
341            continue;
342        }
343        if ch[j] == q {
344            return j + 1;
345        }
346        j += 1;
347    }
348    len
349}
350
351/// ¿El patrón `pat` empieza en `i`?
352fn empieza(ch: &[char], i: usize, pat: &str) -> bool {
353    let mut j = i;
354    for p in pat.chars() {
355        if j >= ch.len() || ch[j] != p {
356            return false;
357        }
358        j += 1;
359    }
360    true
361}
362
363/// Primera posición ≥ `desde` donde aparece `pat`.
364fn buscar(ch: &[char], desde: usize, pat: &str) -> Option<usize> {
365    let n = pat.chars().count();
366    if n == 0 || ch.len() < n {
367        return None;
368    }
369    (desde..=ch.len().saturating_sub(n)).find(|&k| empieza(ch, k, pat))
370}
371
372// =============================================================================
373//  Las tablas
374// =============================================================================
375
376/// La [`Sintaxis`] de un lenguaje de expresiones, o `None` si el suyo es un
377/// tokenizador propio (o si no tiene ninguno).
378pub fn tabla(lang: Language) -> Option<&'static Sintaxis> {
379    Some(match lang {
380        Language::Rust => &RUST,
381        Language::Python => &PYTHON,
382        Language::JavaScript | Language::TypeScript => &JS,
383        Language::Go => &GO,
384        Language::C | Language::Cpp => &C,
385        Language::Java | Language::Kotlin => &JAVA,
386        Language::Ruby => &RUBY,
387        Language::Php => &PHP,
388        Language::Swift => &SWIFT,
389        Language::Lua => &LUA,
390        Language::Zig => &ZIG,
391        Language::Haskell => &HASKELL,
392        Language::Shell => &SHELL,
393        Language::Sql => &SQL,
394        Language::Rhai => &RHAI,
395        // La segunda tanda (2026-09-04).
396        Language::CSharp => &CSHARP,
397        Language::Scala => &SCALA,
398        Language::Dart => &DART,
399        Language::Nim => &NIM,
400        Language::Elixir => &ELIXIR,
401        Language::Erlang => &ERLANG,
402        Language::Clojure => &CLOJURE,
403        Language::R => &ERRE,
404        Language::Perl => &PERL,
405        Language::Julia => &JULIA,
406        Language::OCaml => &OCAML,
407        Language::Elisp => &ELISP,
408        Language::VimScript => &VIMSCRIPT,
409        Language::Asm => &ASM,
410        Language::PowerShell => &POWERSHELL,
411        Language::Batch => &BATCH,
412        Language::Make => &MAKE,
413        Language::CMake => &CMAKE,
414        Language::Dockerfile => &DOCKERFILE,
415        Language::Nix => &NIX,
416        Language::Hcl => &HCL,
417        Language::Proto => &PROTO,
418        Language::GraphQl => &GRAPHQL,
419        Language::Tex => &TEX,
420        Language::Groovy => &GROOVY,
421        Language::Crystal => &CRYSTAL,
422        _ => return None,
423    })
424}
425
426pub static RUST: Sintaxis = Sintaxis {
427    palabras: &[
428        "as", "async", "await", "break", "const", "continue", "crate", "dyn", "else", "enum",
429        "extern", "fn", "for", "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut",
430        "pub", "ref", "return", "self", "Self", "static", "struct", "super", "trait", "type",
431        "unsafe", "use", "where", "while", "yield", "macro_rules",
432    ],
433    tipos: &[
434        "bool", "char", "f32", "f64", "i8", "i16", "i32", "i64", "i128", "isize", "str", "u8",
435        "u16", "u32", "u64", "u128", "usize", "String", "Vec", "Option", "Result", "Box", "Arc",
436        "Rc", "RefCell", "Cell", "HashMap", "HashSet", "BTreeMap",
437    ],
438    constantes: &["true", "false", "None", "Some", "Ok", "Err"],
439    ident_extra: &['!'],
440    inicio_extra: &[],
441    tipo_por_mayuscula: true,
442    ..Sintaxis::base()
443};
444
445pub static PYTHON: Sintaxis = Sintaxis {
446    palabras: &[
447        "and", "as", "assert", "async", "await", "break", "class", "continue", "def", "del",
448        "elif", "else", "except", "finally", "for", "from", "global", "if", "import", "in", "is",
449        "lambda", "match", "nonlocal", "not", "or", "pass", "raise", "return", "try", "while",
450        "with", "yield",
451    ],
452    tipos: &["bool", "bytes", "dict", "float", "int", "list", "set", "str", "tuple", "object"],
453    constantes: &["True", "False", "None", "self", "cls"],
454    comentario_linea: &["#"],
455    comentario_bloque: None,
456    triples: true,
457    anotacion: &['@'],
458    ..Sintaxis::base()
459};
460
461pub static JS: Sintaxis = Sintaxis {
462    palabras: &[
463        "as", "async", "await", "break", "case", "catch", "class", "const", "continue", "debugger",
464        "default", "delete", "do", "else", "enum", "export", "extends", "finally", "for",
465        "function", "from", "if", "implements", "import", "in", "instanceof", "interface", "let",
466        "new", "of", "private", "protected", "public", "readonly", "return", "static", "super",
467        "switch", "this", "throw", "try", "type", "typeof", "var", "void", "while", "yield",
468    ],
469    tipos: &[
470        "any", "bigint", "boolean", "never", "number", "object", "string", "symbol", "unknown",
471        "Array", "Promise", "Map", "Set", "Date", "RegExp", "JSON", "Math",
472    ],
473    constantes: &["true", "false", "null", "undefined", "NaN", "Infinity"],
474    comillas: &['"', '\'', '`'],
475    inicio_extra: &['$'],
476    tipo_por_mayuscula: true,
477    anotacion: &['@'],
478    ..Sintaxis::base()
479};
480
481pub static GO: Sintaxis = Sintaxis {
482    palabras: &[
483        "break", "case", "chan", "const", "continue", "default", "defer", "else", "fallthrough",
484        "for", "func", "go", "goto", "if", "import", "interface", "map", "package", "range",
485        "return", "select", "struct", "switch", "type", "var",
486    ],
487    tipos: &[
488        "bool", "byte", "complex64", "complex128", "error", "float32", "float64", "int", "int8",
489        "int16", "int32", "int64", "rune", "string", "uint", "uint8", "uint16", "uint32", "uint64",
490        "uintptr", "any",
491    ],
492    constantes: &["true", "false", "nil", "iota"],
493    comillas: &['"', '\'', '`'],
494    ..Sintaxis::base()
495};
496
497pub static C: Sintaxis = Sintaxis {
498    palabras: &[
499        "auto", "break", "case", "catch", "class", "const", "constexpr", "continue", "default",
500        "delete", "do", "else", "enum", "explicit", "extern", "for", "friend", "goto", "if",
501        "inline", "namespace", "new", "operator", "private", "protected", "public", "register",
502        "return", "sizeof", "static", "struct", "switch", "template", "this", "throw", "try",
503        "typedef", "typename", "union", "using", "virtual", "volatile", "while",
504    ],
505    tipos: &[
506        "bool", "char", "double", "float", "int", "long", "short", "signed", "size_t", "unsigned",
507        "void", "wchar_t", "int8_t", "int16_t", "int32_t", "int64_t", "uint8_t", "uint16_t",
508        "uint32_t", "uint64_t",
509    ],
510    constantes: &["true", "false", "NULL", "nullptr"],
511    preprocesador: true,
512    ..Sintaxis::base()
513};
514
515pub static JAVA: Sintaxis = Sintaxis {
516    palabras: &[
517        "abstract", "as", "break", "case", "catch", "class", "companion", "const", "continue",
518        "data", "default", "do", "else", "enum", "extends", "final", "finally", "for", "fun",
519        "if", "implements", "import", "in", "instanceof", "interface", "internal", "is", "lateinit",
520        "native", "new", "object", "open", "override", "package", "private", "protected", "public",
521        "return", "sealed", "static", "super", "suspend", "switch", "synchronized", "this",
522        "throw", "throws", "try", "val", "var", "when", "while",
523    ],
524    tipos: &[
525        "boolean", "byte", "char", "double", "float", "int", "long", "short", "void", "Boolean",
526        "Byte", "Char", "Double", "Float", "Int", "Long", "Short", "String", "Unit", "Any",
527        "List", "Map", "Set",
528    ],
529    constantes: &["true", "false", "null", "this", "it"],
530    tipo_por_mayuscula: true,
531    anotacion: &['@'],
532    ..Sintaxis::base()
533};
534
535pub static RUBY: Sintaxis = Sintaxis {
536    palabras: &[
537        "alias", "and", "begin", "break", "case", "class", "def", "defined?", "do", "else",
538        "elsif", "end", "ensure", "for", "if", "in", "module", "next", "not", "or", "redo",
539        "require", "require_relative", "rescue", "retry", "return", "self", "super", "then",
540        "unless", "until", "when", "while", "yield",
541    ],
542    tipos: &["Array", "Hash", "String", "Symbol", "Integer", "Float", "Struct", "Range"],
543    constantes: &["true", "false", "nil", "__FILE__"],
544    comentario_linea: &["#"],
545    comentario_bloque: None,
546    ident_extra: &['?', '!'],
547    inicio_extra: &['@', '$'],
548    tipo_por_mayuscula: true,
549    ..Sintaxis::base()
550};
551
552pub static PHP: Sintaxis = Sintaxis {
553    palabras: &[
554        "abstract", "and", "as", "break", "case", "catch", "class", "const", "continue",
555        "declare", "default", "do", "echo", "else", "elseif", "empty", "endif", "enum", "extends",
556        "final", "finally", "fn", "for", "foreach", "function", "global", "if", "implements",
557        "include", "instanceof", "interface", "isset", "list", "match", "namespace", "new", "or",
558        "print", "private", "protected", "public", "readonly", "require", "return", "static",
559        "switch", "throw", "trait", "try", "unset", "use", "var", "while", "yield",
560    ],
561    tipos: &["array", "bool", "callable", "float", "int", "iterable", "mixed", "object", "string", "void"],
562    constantes: &["true", "false", "null", "TRUE", "FALSE", "NULL", "$this"],
563    comentario_linea: &["//", "#"],
564    inicio_extra: &['$'],
565    tipo_por_mayuscula: true,
566    anotacion: &['@'],
567    ..Sintaxis::base()
568};
569
570pub static SWIFT: Sintaxis = Sintaxis {
571    palabras: &[
572        "as", "associatedtype", "break", "case", "catch", "class", "continue", "default", "defer",
573        "deinit", "do", "else", "enum", "extension", "fallthrough", "for", "func", "guard", "if",
574        "import", "in", "init", "inout", "internal", "is", "let", "open", "operator", "private",
575        "protocol", "public", "repeat", "return", "self", "static", "struct", "subscript",
576        "switch", "throw", "throws", "try", "typealias", "var", "where", "while",
577    ],
578    tipos: &["Any", "Bool", "Character", "Double", "Float", "Int", "String", "Array", "Dictionary", "Set", "Optional"],
579    constantes: &["true", "false", "nil"],
580    tipo_por_mayuscula: true,
581    anotacion: &['@'],
582    ..Sintaxis::base()
583};
584
585pub static LUA: Sintaxis = Sintaxis {
586    palabras: &[
587        "and", "break", "do", "else", "elseif", "end", "for", "function", "goto", "if", "in",
588        "local", "not", "or", "repeat", "return", "then", "until", "while",
589    ],
590    tipos: &["string", "table", "number", "boolean", "function", "thread", "userdata"],
591    constantes: &["true", "false", "nil", "self"],
592    comentario_linea: &["--"],
593    comentario_bloque: Some(("--[[", "]]")),
594    ..Sintaxis::base()
595};
596
597pub static ZIG: Sintaxis = Sintaxis {
598    palabras: &[
599        "align", "and", "asm", "break", "catch", "comptime", "const", "continue", "defer", "else",
600        "enum", "errdefer", "error", "export", "extern", "fn", "for", "if", "inline", "or",
601        "orelse", "packed", "pub", "return", "struct", "switch", "test", "try", "union", "unreachable",
602        "usingnamespace", "var", "while",
603    ],
604    tipos: &[
605        "anyerror", "anytype", "bool", "c_int", "comptime_int", "f16", "f32", "f64", "i8", "i16",
606        "i32", "i64", "isize", "type", "u8", "u16", "u32", "u64", "usize", "void", "noreturn",
607    ],
608    constantes: &["true", "false", "null", "undefined"],
609    comentario_bloque: None,
610    tipo_por_mayuscula: true,
611    ..Sintaxis::base()
612};
613
614pub static HASKELL: Sintaxis = Sintaxis {
615    palabras: &[
616        "case", "class", "data", "default", "deriving", "do", "else", "foreign", "if", "import",
617        "in", "infix", "infixl", "infixr", "instance", "let", "module", "newtype", "of", "then",
618        "type", "where",
619    ],
620    tipos: &["Bool", "Char", "Double", "Float", "Int", "Integer", "IO", "Maybe", "String", "Either"],
621    constantes: &["True", "False", "Nothing", "Just"],
622    comentario_linea: &["--"],
623    comentario_bloque: Some(("{-", "-}")),
624    ident_extra: &['\''],
625    tipo_por_mayuscula: true,
626    ..Sintaxis::base()
627};
628
629pub static SHELL: Sintaxis = Sintaxis {
630    palabras: &[
631        "case", "do", "done", "elif", "else", "esac", "exit", "export", "fi", "for", "function",
632        "if", "in", "local", "read", "readonly", "return", "select", "set", "shift", "source",
633        "then", "trap", "unset", "until", "while",
634    ],
635    tipos: &[
636        "awk", "cat", "cd", "chmod", "cp", "curl", "cut", "echo", "find", "grep", "head", "ls",
637        "mkdir", "mv", "printf", "rm", "sed", "sort", "tail", "test", "tr", "uniq", "wc", "xargs",
638    ],
639    constantes: &["true", "false"],
640    comentario_linea: &["#"],
641    comentario_bloque: None,
642    comillas: &['"', '\''],
643    inicio_extra: &['$'],
644    ..Sintaxis::base()
645};
646
647pub static SQL: Sintaxis = Sintaxis {
648    palabras: &[
649        "add", "all", "alter", "and", "as", "asc", "begin", "between", "by", "case", "commit",
650        "constraint", "create", "cross", "delete", "desc", "distinct", "drop", "else", "end",
651        "exists", "foreign", "from", "full", "group", "having", "if", "in", "index", "inner",
652        "insert", "into", "is", "join", "key", "left", "like", "limit", "not", "null", "offset",
653        "on", "or", "order", "outer", "primary", "references", "returning", "right", "rollback",
654        "select", "set", "table", "then", "transaction", "union", "unique", "update", "using",
655        "values", "view", "when", "where", "with",
656    ],
657    tipos: &[
658        "bigint", "blob", "boolean", "bytea", "char", "date", "decimal", "double", "float", "int",
659        "integer", "json", "jsonb", "numeric", "real", "serial", "smallint", "text", "time",
660        "timestamp", "uuid", "varchar",
661    ],
662    constantes: &["true", "false", "null"],
663    comentario_linea: &["--"],
664    ignora_caja: true,
665    comillas: &['\'', '"'],
666    ..Sintaxis::base()
667};
668
669pub static RHAI: Sintaxis = Sintaxis {
670    palabras: &[
671        "break", "const", "continue", "do", "else", "export", "fn", "for", "if", "import", "in",
672        "let", "loop", "private", "return", "switch", "this", "throw", "try", "catch", "while",
673    ],
674    tipos: &["bool", "char", "float", "int", "string", "array", "map", "timestamp"],
675    constantes: &["true", "false", "()"],
676    ..Sintaxis::base()
677};
678
679// -----------------------------------------------------------------------------
680//  La segunda tanda (2026-09-04) — *«refuerza todos los lenguajes de
681//  programación»*
682//
683//  Todas por tabla y en Rust puro: cada una es una constante, no una gramática,
684//  así que la matriz pasa de veintiocho lenguajes a cincuenta y cuatro sin
685//  sumar un solo byte de C ni romper el cross-compile.
686// -----------------------------------------------------------------------------
687
688pub static CSHARP: Sintaxis = Sintaxis {
689    palabras: &[
690        "abstract", "as", "async", "await", "base", "break", "case", "catch", "checked", "class",
691        "const", "continue", "default", "delegate", "do", "else", "enum", "event", "explicit",
692        "extern", "finally", "fixed", "for", "foreach", "get", "goto", "if", "implicit", "in",
693        "interface", "internal", "is", "lock", "namespace", "new", "operator", "out", "override",
694        "params", "partial", "private", "protected", "public", "readonly", "record", "ref",
695        "return", "sealed", "set", "sizeof", "stackalloc", "static", "struct", "switch", "this",
696        "throw", "try", "typeof", "unchecked", "unsafe", "using", "var", "virtual", "void",
697        "volatile", "when", "where", "while", "yield",
698    ],
699    tipos: &[
700        "bool", "byte", "char", "decimal", "double", "float", "int", "long", "object", "sbyte",
701        "short", "string", "uint", "ulong", "ushort", "nint", "nuint", "dynamic",
702    ],
703    constantes: &["true", "false", "null", "value"],
704    tipo_por_mayuscula: true,
705    ..Sintaxis::base()
706};
707
708pub static SCALA: Sintaxis = Sintaxis {
709    palabras: &[
710        "abstract", "case", "catch", "class", "def", "do", "else", "extends", "final", "finally",
711        "for", "forSome", "given", "if", "implicit", "import", "lazy", "match", "new", "object",
712        "override", "package", "private", "protected", "return", "sealed", "super", "then",
713        "this", "throw", "trait", "try", "type", "using", "val", "var", "while", "with", "yield",
714        "enum", "export", "inline", "opaque",
715    ],
716    tipos: &[
717        "Boolean", "Byte", "Char", "Double", "Float", "Int", "Long", "Short", "String", "Unit",
718        "Any", "AnyRef", "Nothing", "Option", "Either", "List", "Seq", "Map", "Set", "Vector",
719    ],
720    constantes: &["true", "false", "null", "None", "Some", "Nil"],
721    triples: true,
722    tipo_por_mayuscula: true,
723    anotacion: &['@'],
724    ..Sintaxis::base()
725};
726
727pub static DART: Sintaxis = Sintaxis {
728    palabras: &[
729        "abstract", "as", "assert", "async", "await", "base", "break", "case", "catch", "class",
730        "const", "continue", "covariant", "default", "deferred", "do", "else", "enum", "export",
731        "extends", "extension", "external", "factory", "final", "finally", "for", "get", "hide",
732        "if", "implements", "import", "in", "interface", "is", "late", "library", "mixin", "new",
733        "on", "operator", "part", "required", "rethrow", "return", "sealed", "set", "show",
734        "static", "super", "switch", "sync", "this", "throw", "try", "typedef", "var", "when",
735        "while", "with", "yield",
736    ],
737    tipos: &[
738        "bool", "double", "dynamic", "int", "num", "void", "String", "List", "Map", "Set",
739        "Future", "Stream", "Object", "Never", "Iterable",
740    ],
741    constantes: &["true", "false", "null"],
742    triples: true,
743    tipo_por_mayuscula: true,
744    anotacion: &['@'],
745    ..Sintaxis::base()
746};
747
748pub static NIM: Sintaxis = Sintaxis {
749    palabras: &[
750        "addr", "and", "as", "asm", "bind", "block", "break", "case", "cast", "concept", "const",
751        "continue", "converter", "defer", "discard", "distinct", "div", "do", "elif", "else",
752        "end", "enum", "except", "export", "finally", "for", "from", "func", "if", "import", "in",
753        "include", "interface", "is", "isnot", "iterator", "let", "macro", "method", "mixin",
754        "mod", "not", "notin", "object", "of", "or", "out", "proc", "ptr", "raise", "ref",
755        "return", "shl", "shr", "static", "template", "try", "tuple", "type", "using", "var",
756        "when", "while", "xor", "yield",
757    ],
758    tipos: &[
759        "int", "int8", "int16", "int32", "int64", "uint", "uint8", "uint16", "uint32", "uint64",
760        "float", "float32", "float64", "bool", "char", "string", "cstring", "seq", "array",
761        "openArray", "pointer",
762    ],
763    constantes: &["true", "false", "nil", "result"],
764    comentario_linea: &["#"],
765    comentario_bloque: Some(("#[", "]#")),
766    triples: true,
767    tipo_por_mayuscula: true,
768    ..Sintaxis::base()
769};
770
771pub static ELIXIR: Sintaxis = Sintaxis {
772    palabras: &[
773        "after", "and", "case", "catch", "cond", "def", "defdelegate", "defexception", "defguard",
774        "defimpl", "defmacro", "defmodule", "defp", "defprotocol", "defstruct", "do", "else",
775        "end", "fn", "for", "if", "import", "in", "not", "or", "quote", "raise", "receive",
776        "require", "rescue", "throw", "try", "unless", "unquote", "use", "when", "with",
777    ],
778    tipos: &["atom", "binary", "bitstring", "boolean", "float", "integer", "list", "map", "pid", "tuple"],
779    constantes: &["true", "false", "nil"],
780    comentario_linea: &["#"],
781    comentario_bloque: None,
782    triples: true,
783    ident_extra: &['?', '!'],
784    tipo_por_mayuscula: true,
785    anotacion: &['@'],
786    ..Sintaxis::base()
787};
788
789pub static ERLANG: Sintaxis = Sintaxis {
790    palabras: &[
791        "after", "and", "andalso", "band", "begin", "bnot", "bor", "bsl", "bsr", "bxor", "case",
792        "catch", "cond", "div", "end", "fun", "if", "let", "not", "of", "or", "orelse", "receive",
793        "rem", "try", "when", "xor",
794    ],
795    tipos: &["atom", "binary", "boolean", "float", "integer", "list", "map", "pid", "tuple", "term"],
796    constantes: &["true", "false", "undefined", "ok", "error"],
797    comentario_linea: &["%"],
798    comentario_bloque: None,
799    tipo_por_mayuscula: true,
800    ..Sintaxis::base()
801};
802
803pub static CLOJURE: Sintaxis = Sintaxis {
804    palabras: &[
805        "def", "defn", "defn-", "defmacro", "defmulti", "defmethod", "defprotocol", "defrecord",
806        "deftype", "defstruct", "do", "doseq", "dotimes", "fn", "for", "if", "if-let", "if-not",
807        "let", "letfn", "loop", "ns", "quote", "recur", "require", "set!", "throw", "try",
808        "catch", "finally", "var", "when", "when-let", "when-not", "while", "cond", "condp",
809        "case", "->", "->>", "import", "in-ns", "reify", "binding",
810    ],
811    tipos: &[],
812    constantes: &["true", "false", "nil"],
813    comentario_linea: &[";"],
814    comentario_bloque: None,
815    comillas: &['"'],
816    ident_extra: &['-', '?', '!', '*', '+', '/', '>', '<', '='],
817    inicio_extra: &[':'],
818    ..Sintaxis::base()
819};
820
821/// R. Se llama `ERRE` porque `R` a secas no se lee como el nombre de una tabla.
822pub static ERRE: Sintaxis = Sintaxis {
823    palabras: &[
824        "break", "else", "for", "function", "if", "in", "next", "repeat", "return", "while",
825        "library", "require", "source", "switch",
826    ],
827    tipos: &["character", "complex", "double", "integer", "logical", "numeric", "raw", "list", "factor"],
828    constantes: &["TRUE", "FALSE", "NULL", "NA", "Inf", "NaN", "T", "F"],
829    comentario_linea: &["#"],
830    comentario_bloque: None,
831    ident_extra: &['.'],
832    ..Sintaxis::base()
833};
834
835pub static PERL: Sintaxis = Sintaxis {
836    palabras: &[
837        "and", "cmp", "continue", "do", "else", "elsif", "eq", "eval", "exit", "for", "foreach",
838        "ge", "gt", "if", "last", "le", "local", "lt", "my", "ne", "next", "no", "not", "or",
839        "our", "package", "print", "redo", "ref", "require", "return", "say", "sub", "unless",
840        "until", "use", "wantarray", "while", "qw", "qq",
841    ],
842    tipos: &["scalar", "defined", "undef", "bless"],
843    constantes: &["__PACKAGE__", "__END__", "__DATA__", "STDIN", "STDOUT", "STDERR"],
844    comentario_linea: &["#"],
845    comentario_bloque: None,
846    inicio_extra: &['$', '@', '%'],
847    ..Sintaxis::base()
848};
849
850pub static JULIA: Sintaxis = Sintaxis {
851    palabras: &[
852        "abstract", "baremodule", "begin", "break", "catch", "const", "continue", "do", "else",
853        "elseif", "end", "export", "finally", "for", "function", "global", "if", "import", "in",
854        "let", "local", "macro", "module", "mutable", "primitive", "quote", "return", "struct",
855        "try", "type", "using", "where", "while",
856    ],
857    tipos: &[
858        "Int", "Int8", "Int16", "Int32", "Int64", "UInt", "Float16", "Float32", "Float64", "Bool",
859        "Char", "String", "Symbol", "Array", "Vector", "Matrix", "Dict", "Set", "Tuple", "Any",
860    ],
861    constantes: &["true", "false", "nothing", "missing", "Inf", "NaN"],
862    comentario_linea: &["#"],
863    comentario_bloque: Some(("#=", "=#")),
864    triples: true,
865    ident_extra: &['!'],
866    tipo_por_mayuscula: true,
867    ..Sintaxis::base()
868};
869
870pub static OCAML: Sintaxis = Sintaxis {
871    palabras: &[
872        "and", "as", "assert", "begin", "class", "constraint", "do", "done", "downto", "else",
873        "end", "exception", "external", "for", "fun", "function", "functor", "if", "in",
874        "include", "inherit", "initializer", "lazy", "let", "match", "method", "module",
875        "mutable", "new", "nonrec", "object", "of", "open", "private", "rec", "sig", "struct",
876        "then", "to", "try", "type", "val", "virtual", "when", "while", "with",
877    ],
878    tipos: &["bool", "char", "float", "int", "list", "option", "string", "unit", "array", "ref"],
879    constantes: &["true", "false", "None", "Some", "()"],
880    // OCaml no tiene comentario de línea: `(* … *)` es todo lo que hay.
881    comentario_linea: &[],
882    comentario_bloque: Some(("(*", "*)")),
883    comillas: &['"'],
884    ident_extra: &['\''],
885    tipo_por_mayuscula: true,
886    ..Sintaxis::base()
887};
888
889pub static ELISP: Sintaxis = Sintaxis {
890    palabras: &[
891        "defun", "defvar", "defcustom", "defconst", "defmacro", "defgroup", "defface", "lambda",
892        "let", "let*", "if", "when", "unless", "cond", "while", "dolist", "dotimes", "progn",
893        "prog1", "setq", "setq-default", "require", "provide", "interactive", "save-excursion",
894        "with-current-buffer", "condition-case", "unwind-protect", "and", "or", "not", "quote",
895    ],
896    tipos: &[],
897    constantes: &["t", "nil"],
898    comentario_linea: &[";"],
899    comentario_bloque: None,
900    comillas: &['"'],
901    ident_extra: &['-', '?', '!', '*', '+', '/', '>', '<', '='],
902    ..Sintaxis::base()
903};
904
905/// VimScript. El comentario de vim legacy abre con `"`, la misma comilla que
906/// una cadena — no hay forma de resolverlo sin parsear el contexto entero. Se
907/// elige el comentario (es lo que domina en un `.vimrc`) y la cadena queda en
908/// la comilla simple, que es la que vim recomienda para literales.
909pub static VIMSCRIPT: Sintaxis = Sintaxis {
910    palabras: &[
911        "if", "elseif", "else", "endif", "while", "endwhile", "for", "endfor", "function",
912        "endfunction", "return", "let", "unlet", "call", "execute", "set", "setlocal", "map",
913        "nmap", "vmap", "imap", "noremap", "nnoremap", "inoremap", "vnoremap", "autocmd",
914        "augroup", "source", "try", "catch", "endtry", "finally", "throw", "echo", "echom",
915        "command", "highlight", "syntax", "normal", "vim9script", "def", "enddef", "export",
916        "import", "const", "var",
917    ],
918    tipos: &["number", "string", "list", "dict", "func", "bool", "blob"],
919    constantes: &["v:true", "v:false", "v:null"],
920    comentario_linea: &["\"", "#"],
921    comentario_bloque: None,
922    comillas: &['\''],
923    inicio_extra: &['&', '$'],
924    ident_extra: &[':', '#'],
925    ..Sintaxis::base()
926};
927
928pub static ASM: Sintaxis = Sintaxis {
929    palabras: &[
930        "mov", "movq", "movl", "movb", "lea", "push", "pop", "call", "ret", "jmp", "je", "jne",
931        "jz", "jnz", "jg", "jl", "jge", "jle", "cmp", "test", "add", "sub", "mul", "imul", "div",
932        "idiv", "inc", "dec", "and", "or", "xor", "not", "shl", "shr", "sar", "nop", "hlt", "int",
933        "syscall", "leave", "section", "global", "extern", "db", "dw", "dd", "dq", "equ", "times",
934        "resb", "align", "byte", "word", "dword", "qword",
935    ],
936    tipos: &[
937        "rax", "rbx", "rcx", "rdx", "rsi", "rdi", "rsp", "rbp", "eax", "ebx", "ecx", "edx", "esi",
938        "edi", "esp", "ebp", "ax", "bx", "cx", "dx", "al", "bl", "cl", "dl", "r8", "r9", "r10",
939        "r11", "r12", "r13", "r14", "r15", "xmm0", "xmm1", "xmm2", "xmm3",
940    ],
941    constantes: &[],
942    comentario_linea: &[";", "#", "//"],
943    comentario_bloque: Some(("/*", "*/")),
944    inicio_extra: &['%', '.'],
945    ..Sintaxis::base()
946};
947
948pub static POWERSHELL: Sintaxis = Sintaxis {
949    palabras: &[
950        "begin", "break", "catch", "class", "continue", "data", "do", "dynamicparam", "else",
951        "elseif", "end", "enum", "exit", "filter", "finally", "for", "foreach", "from",
952        "function", "if", "in", "param", "process", "return", "switch", "throw", "trap", "try",
953        "until", "using", "while", "workflow",
954    ],
955    tipos: &[
956        "string", "int", "long", "bool", "double", "decimal", "char", "byte", "array",
957        "hashtable", "psobject", "scriptblock", "void",
958    ],
959    constantes: &["$true", "$false", "$null", "$_", "$args", "$PSItem"],
960    comentario_linea: &["#"],
961    comentario_bloque: Some(("<#", "#>")),
962    inicio_extra: &['$'],
963    ident_extra: &['-'],
964    ignora_caja: true,
965    ..Sintaxis::base()
966};
967
968pub static BATCH: Sintaxis = Sintaxis {
969    palabras: &[
970        "call", "echo", "endlocal", "exit", "for", "goto", "if", "in", "do", "not", "pause",
971        "set", "setlocal", "shift", "start", "cd", "copy", "del", "dir", "md", "rd", "ren",
972        "type", "errorlevel", "defined", "exist", "else",
973    ],
974    tipos: &[],
975    constantes: &["nul", "on", "off"],
976    comentario_linea: &["rem ", "::"],
977    comentario_bloque: None,
978    comillas: &['"'],
979    inicio_extra: &['%'],
980    ignora_caja: true,
981    ..Sintaxis::base()
982};
983
984pub static MAKE: Sintaxis = Sintaxis {
985    palabras: &[
986        "ifeq", "ifneq", "ifdef", "ifndef", "else", "endif", "include", "-include", "sinclude",
987        "define", "endef", "export", "unexport", "override", "vpath", "wildcard", "shell",
988        "patsubst", "subst", "foreach", "call", "eval", "value", "origin", "notdir", "dir",
989        "basename", "suffix", "addprefix", "addsuffix",
990    ],
991    tipos: &[],
992    constantes: &[],
993    comentario_linea: &["#"],
994    comentario_bloque: None,
995    inicio_extra: &['$', '.', '@'],
996    ident_extra: &['-'],
997    ..Sintaxis::base()
998};
999
1000pub static CMAKE: Sintaxis = Sintaxis {
1001    palabras: &[
1002        "add_executable", "add_library", "add_subdirectory", "add_custom_command",
1003        "add_custom_target", "add_definitions", "cmake_minimum_required", "else", "elseif",
1004        "endforeach", "endfunction", "endif", "endmacro", "endwhile", "find_package",
1005        "find_library", "foreach", "function", "if", "include", "include_directories", "install",
1006        "list", "macro", "message", "option", "project", "return", "set", "set_target_properties",
1007        "string", "target_compile_options", "target_include_directories", "target_link_libraries",
1008        "while",
1009    ],
1010    tipos: &["BOOL", "STRING", "PATH", "FILEPATH", "INTERNAL", "PUBLIC", "PRIVATE", "INTERFACE"],
1011    constantes: &["ON", "OFF", "TRUE", "FALSE", "NOTFOUND", "REQUIRED", "QUIET"],
1012    comentario_linea: &["#"],
1013    comentario_bloque: None,
1014    comillas: &['"'],
1015    inicio_extra: &['$'],
1016    ignora_caja: true,
1017    ..Sintaxis::base()
1018};
1019
1020pub static DOCKERFILE: Sintaxis = Sintaxis {
1021    palabras: &[
1022        "FROM", "RUN", "CMD", "LABEL", "MAINTAINER", "EXPOSE", "ENV", "ADD", "COPY", "ENTRYPOINT",
1023        "VOLUME", "USER", "WORKDIR", "ARG", "ONBUILD", "STOPSIGNAL", "HEALTHCHECK", "SHELL", "AS",
1024    ],
1025    tipos: &[],
1026    constantes: &[],
1027    comentario_linea: &["#"],
1028    comentario_bloque: None,
1029    inicio_extra: &['$'],
1030    ignora_caja: true,
1031    ..Sintaxis::base()
1032};
1033
1034pub static NIX: Sintaxis = Sintaxis {
1035    palabras: &[
1036        "assert", "else", "if", "in", "inherit", "let", "or", "rec", "then", "with", "import",
1037        "builtins", "derivation", "mkDerivation",
1038    ],
1039    tipos: &[],
1040    constantes: &["true", "false", "null"],
1041    comentario_linea: &["#"],
1042    comentario_bloque: Some(("/*", "*/")),
1043    ident_extra: &['-', '\''],
1044    ..Sintaxis::base()
1045};
1046
1047/// HCL / Terraform.
1048pub static HCL: Sintaxis = Sintaxis {
1049    palabras: &[
1050        "resource", "provider", "variable", "output", "module", "data", "locals", "terraform",
1051        "for", "for_each", "if", "in", "dynamic", "content", "depends_on", "count", "provisioner",
1052        "backend", "required_providers",
1053    ],
1054    tipos: &["string", "number", "bool", "list", "map", "set", "object", "tuple", "any"],
1055    constantes: &["true", "false", "null"],
1056    comentario_linea: &["#", "//"],
1057    comentario_bloque: Some(("/*", "*/")),
1058    ident_extra: &['-'],
1059    ..Sintaxis::base()
1060};
1061
1062pub static PROTO: Sintaxis = Sintaxis {
1063    palabras: &[
1064        "syntax", "package", "import", "option", "message", "enum", "service", "rpc", "returns",
1065        "repeated", "optional", "required", "oneof", "map", "reserved", "extend", "extensions",
1066        "public", "weak", "stream",
1067    ],
1068    tipos: &[
1069        "double", "float", "int32", "int64", "uint32", "uint64", "sint32", "sint64", "fixed32",
1070        "fixed64", "sfixed32", "sfixed64", "bool", "string", "bytes",
1071    ],
1072    constantes: &["true", "false"],
1073    tipo_por_mayuscula: true,
1074    ..Sintaxis::base()
1075};
1076
1077pub static GRAPHQL: Sintaxis = Sintaxis {
1078    palabras: &[
1079        "query", "mutation", "subscription", "fragment", "on", "type", "input", "enum",
1080        "interface", "union", "schema", "scalar", "implements", "extend", "directive", "repeatable",
1081    ],
1082    tipos: &["Int", "Float", "String", "Boolean", "ID"],
1083    constantes: &["true", "false", "null"],
1084    comentario_linea: &["#"],
1085    comentario_bloque: None,
1086    comillas: &['"'],
1087    triples: true,
1088    tipo_por_mayuscula: true,
1089    anotacion: &['@'],
1090    ..Sintaxis::base()
1091};
1092
1093pub static TEX: Sintaxis = Sintaxis {
1094    palabras: &[
1095        "\\begin", "\\end", "\\documentclass", "\\usepackage", "\\section", "\\subsection",
1096        "\\subsubsection", "\\chapter", "\\part", "\\paragraph", "\\newcommand",
1097        "\\renewcommand", "\\newenvironment", "\\label", "\\ref", "\\cite", "\\item",
1098        "\\textbf", "\\textit", "\\emph", "\\includegraphics", "\\input", "\\include",
1099        "\\caption", "\\footnote", "\\title", "\\author", "\\date", "\\maketitle",
1100    ],
1101    tipos: &[],
1102    constantes: &[],
1103    comentario_linea: &["%"],
1104    comentario_bloque: None,
1105    // En TeX no hay cadenas: las comillas son texto corriente y teñirlas de
1106    // verde pinta párrafos enteros.
1107    comillas: &[],
1108    inicio_extra: &['\\'],
1109    ..Sintaxis::base()
1110};
1111
1112pub static GROOVY: Sintaxis = Sintaxis {
1113    palabras: &[
1114        "abstract", "as", "assert", "break", "case", "catch", "class", "const", "continue", "def",
1115        "default", "do", "else", "enum", "extends", "final", "finally", "for", "goto", "if",
1116        "implements", "import", "in", "instanceof", "interface", "native", "new", "package",
1117        "private", "protected", "public", "return", "static", "super", "switch", "synchronized",
1118        "this", "throw", "throws", "trait", "transient", "try", "void", "volatile", "while",
1119    ],
1120    tipos: &[
1121        "boolean", "byte", "char", "double", "float", "int", "long", "short", "String", "List",
1122        "Map", "Set", "Object", "BigDecimal", "BigInteger",
1123    ],
1124    constantes: &["true", "false", "null", "it"],
1125    triples: true,
1126    tipo_por_mayuscula: true,
1127    anotacion: &['@'],
1128    ..Sintaxis::base()
1129};
1130
1131pub static CRYSTAL: Sintaxis = Sintaxis {
1132    palabras: &[
1133        "abstract", "alias", "annotation", "as", "begin", "break", "case", "class", "def", "do",
1134        "else", "elsif", "end", "ensure", "enum", "extend", "for", "fun", "if", "in", "include",
1135        "instance_sizeof", "is_a?", "lib", "macro", "module", "next", "of", "out", "pointerof",
1136        "private", "protected", "require", "rescue", "return", "select", "self", "sizeof",
1137        "struct", "super", "then", "type", "typeof", "uninitialized", "union", "unless", "until",
1138        "when", "while", "with", "yield",
1139    ],
1140    tipos: &[
1141        "Bool", "Char", "Float32", "Float64", "Int8", "Int16", "Int32", "Int64", "UInt8",
1142        "UInt16", "UInt32", "UInt64", "String", "Symbol", "Array", "Hash", "Set", "Tuple", "Nil",
1143    ],
1144    constantes: &["true", "false", "nil"],
1145    comentario_linea: &["#"],
1146    comentario_bloque: None,
1147    ident_extra: &['?', '!'],
1148    inicio_extra: &['@'],
1149    tipo_por_mayuscula: true,
1150    ..Sintaxis::base()
1151};
1152
1153// =============================================================================
1154//  Los que no son lenguajes de expresiones
1155// =============================================================================
1156
1157/// JSON: la tabla de llaves más un repaso que asciende a **clave** la cadena
1158/// que tiene un `:` a la derecha. Sin eso, clave y valor salen del mismo verde
1159/// y un objeto largo se lee como un bloque.
1160pub fn json(fuente: &str) -> Vec<Vec<Span>> {
1161    static JSON: Sintaxis = Sintaxis {
1162        constantes: &["true", "false", "null"],
1163        comentario_linea: &["//"],
1164        comillas: &['"'],
1165        ..Sintaxis::base()
1166    };
1167    let mut lineas = tokenizar(fuente, &JSON);
1168    for (spans, texto) in lineas.iter_mut().zip(iterate_lines(fuente)) {
1169        let ch: Vec<char> = texto.chars().collect();
1170        for k in 0..spans.len() {
1171            if spans[k].kind != TokenKind::String {
1172                continue;
1173            }
1174            let mut j = spans[k].end_col;
1175            while j < ch.len() && ch[j] == ' ' {
1176                j += 1;
1177            }
1178            if j < ch.len() && ch[j] == ':' {
1179                spans[k].kind = TokenKind::Function;
1180            }
1181        }
1182    }
1183    lineas
1184}
1185
1186/// RON: llaves, comentarios `//`, y los nombres antes de un `:` como claves.
1187pub fn ron(fuente: &str) -> Vec<Vec<Span>> {
1188    static RON: Sintaxis = Sintaxis {
1189        constantes: &["true", "false", "None", "Some"],
1190        comillas: &['"', '\''],
1191        tipo_por_mayuscula: true,
1192        ..Sintaxis::base()
1193    };
1194    let mut lineas = tokenizar(fuente, &RON);
1195    for (spans, texto) in lineas.iter_mut().zip(iterate_lines(fuente)) {
1196        let ch: Vec<char> = texto.chars().collect();
1197        for k in 0..spans.len() {
1198            if spans[k].kind != TokenKind::Identifier {
1199                continue;
1200            }
1201            let mut j = spans[k].end_col;
1202            while j < ch.len() && ch[j] == ' ' {
1203                j += 1;
1204            }
1205            if j < ch.len() && ch[j] == ':' {
1206                spans[k].kind = TokenKind::Function;
1207            }
1208        }
1209    }
1210    lineas
1211}
1212
1213/// TOML / INI: `[seccion]`, `clave = valor`, `#` o `;` de comentario.
1214pub fn toml(fuente: &str, punto_y_coma: bool) -> Vec<Vec<Span>> {
1215    let mut out: Vec<Vec<Span>> = Vec::new();
1216    for linea in iterate_lines(fuente) {
1217        let ch: Vec<char> = linea.chars().collect();
1218        let len = ch.len();
1219        let mut spans: Vec<Span> = Vec::new();
1220        let primera = ch.iter().position(|c| !c.is_whitespace());
1221        match primera {
1222            None => {
1223                out.push(fill_gaps(spans, len));
1224                continue;
1225            }
1226            Some(p) if ch[p] == '#' || (punto_y_coma && ch[p] == ';') => {
1227                spans.push(Span { start_col: p, end_col: len, kind: TokenKind::Comment });
1228                out.push(fill_gaps(spans, len));
1229                continue;
1230            }
1231            Some(p) if ch[p] == '[' => {
1232                // Encabezado de tabla — hasta el `]` o el fin de línea.
1233                let fin = buscar(&ch, p, "]").map(|k| k + 1).unwrap_or(len);
1234                spans.push(Span { start_col: p, end_col: fin, kind: TokenKind::Type });
1235                spans.extend(valor_toml(&ch, fin));
1236                out.push(fill_gaps(spans, len));
1237                continue;
1238            }
1239            Some(_) => {}
1240        }
1241        // `clave = valor`: la clave es todo lo anterior al primer `=`.
1242        match ch.iter().position(|c| *c == '=') {
1243            Some(eq) => {
1244                spans.push(Span { start_col: 0, end_col: eq, kind: TokenKind::Function });
1245                spans.push(Span { start_col: eq, end_col: eq + 1, kind: TokenKind::Operator });
1246                spans.extend(valor_toml(&ch, eq + 1));
1247            }
1248            None => spans.extend(valor_toml(&ch, 0)),
1249        }
1250        out.push(fill_gaps(spans, len));
1251    }
1252    out
1253}
1254
1255/// El lado derecho de un `clave = valor`: cadenas, números, booleanos y el
1256/// comentario que puede venir al final.
1257fn valor_toml(ch: &[char], desde: usize) -> Vec<Span> {
1258    let len = ch.len();
1259    let mut out = Vec::new();
1260    let mut i = desde;
1261    while i < len {
1262        let c = ch[i];
1263        if c == '#' {
1264            out.push(Span { start_col: i, end_col: len, kind: TokenKind::Comment });
1265            break;
1266        }
1267        if c == '"' || c == '\'' {
1268            let ini = i;
1269            i = fin_de_cadena(ch, i, c);
1270            out.push(Span { start_col: ini, end_col: i, kind: TokenKind::String });
1271            continue;
1272        }
1273        if c.is_ascii_digit() {
1274            let ini = i;
1275            while i < len && (ch[i].is_ascii_alphanumeric() || ch[i] == '.' || ch[i] == '-' || ch[i] == ':' || ch[i] == '+') {
1276                i += 1;
1277            }
1278            out.push(Span { start_col: ini, end_col: i, kind: TokenKind::Number });
1279            continue;
1280        }
1281        if c.is_alphabetic() {
1282            let ini = i;
1283            while i < len && (ch[i].is_alphanumeric() || ch[i] == '_' || ch[i] == '-') {
1284                i += 1;
1285            }
1286            let p: String = ch[ini..i].iter().collect();
1287            let kind = if p == "true" || p == "false" {
1288                TokenKind::Number
1289            } else {
1290                TokenKind::Identifier
1291            };
1292            out.push(Span { start_col: ini, end_col: i, kind });
1293            continue;
1294        }
1295        if "[]{},".contains(c) {
1296            out.push(Span { start_col: i, end_col: i + 1, kind: TokenKind::Punctuation });
1297        }
1298        i += 1;
1299    }
1300    out
1301}
1302
1303/// YAML: `clave:`, `- ítem`, `#` de comentario, anclas `&a`/`*a`.
1304pub fn yaml(fuente: &str) -> Vec<Vec<Span>> {
1305    let mut out: Vec<Vec<Span>> = Vec::new();
1306    for linea in iterate_lines(fuente) {
1307        let ch: Vec<char> = linea.chars().collect();
1308        let len = ch.len();
1309        let mut spans: Vec<Span> = Vec::new();
1310        let mut i = match ch.iter().position(|c| !c.is_whitespace()) {
1311            Some(p) => p,
1312            None => {
1313                out.push(fill_gaps(spans, len));
1314                continue;
1315            }
1316        };
1317        if ch[i] == '#' {
1318            spans.push(Span { start_col: i, end_col: len, kind: TokenKind::Comment });
1319            out.push(fill_gaps(spans, len));
1320            continue;
1321        }
1322        // Guiones de lista, uno o varios (`- - a` es válido).
1323        while i < len && ch[i] == '-' && (i + 1 >= len || ch[i + 1] == ' ') {
1324            spans.push(Span { start_col: i, end_col: i + 1, kind: TokenKind::Punctuation });
1325            i += 1;
1326            while i < len && ch[i] == ' ' {
1327                i += 1;
1328            }
1329        }
1330        // `---` separador de documentos.
1331        if empieza(&ch, i, "---") || empieza(&ch, i, "...") {
1332            spans.push(Span { start_col: i, end_col: len, kind: TokenKind::Punctuation });
1333            out.push(fill_gaps(spans, len));
1334            continue;
1335        }
1336        // ¿Hay clave? Un `:` seguido de espacio o fin de línea.
1337        let clave = (i..len).find(|&k| {
1338            ch[k] == ':' && (k + 1 >= len || ch[k + 1] == ' ' || ch[k + 1] == '\t')
1339        });
1340        let resto = match clave {
1341            Some(k) => {
1342                spans.push(Span { start_col: i, end_col: k, kind: TokenKind::Function });
1343                spans.push(Span { start_col: k, end_col: k + 1, kind: TokenKind::Operator });
1344                k + 1
1345            }
1346            None => i,
1347        };
1348        spans.extend(valor_yaml(&ch, resto));
1349        out.push(fill_gaps(spans, len));
1350    }
1351    out
1352}
1353
1354fn valor_yaml(ch: &[char], desde: usize) -> Vec<Span> {
1355    let len = ch.len();
1356    let mut out = Vec::new();
1357    let mut i = desde;
1358    while i < len {
1359        let c = ch[i];
1360        if c == '#' && (i == 0 || ch[i - 1].is_whitespace()) {
1361            out.push(Span { start_col: i, end_col: len, kind: TokenKind::Comment });
1362            break;
1363        }
1364        if c == '"' || c == '\'' {
1365            let ini = i;
1366            i = fin_de_cadena(ch, i, c);
1367            out.push(Span { start_col: ini, end_col: i, kind: TokenKind::String });
1368            continue;
1369        }
1370        if c == '&' || c == '*' {
1371            let ini = i;
1372            i += 1;
1373            while i < len && (ch[i].is_alphanumeric() || ch[i] == '_' || ch[i] == '-') {
1374                i += 1;
1375            }
1376            out.push(Span { start_col: ini, end_col: i, kind: TokenKind::Type });
1377            continue;
1378        }
1379        if c.is_ascii_digit() {
1380            let ini = i;
1381            while i < len && (ch[i].is_ascii_alphanumeric() || ch[i] == '.' || ch[i] == '-' || ch[i] == ':') {
1382                i += 1;
1383            }
1384            out.push(Span { start_col: ini, end_col: i, kind: TokenKind::Number });
1385            continue;
1386        }
1387        if c.is_alphabetic() {
1388            let ini = i;
1389            while i < len && (ch[i].is_alphanumeric() || ch[i] == '_' || ch[i] == '-' || ch[i] == '.') {
1390                i += 1;
1391            }
1392            let p: String = ch[ini..i].iter().collect();
1393            let kind = match p.as_str() {
1394                "true" | "false" | "yes" | "no" | "on" | "off" | "null" | "~" => TokenKind::Number,
1395                _ => TokenKind::Other,
1396            };
1397            out.push(Span { start_col: ini, end_col: i, kind });
1398            continue;
1399        }
1400        i += 1;
1401    }
1402    out
1403}
1404
1405/// Markdown: encabezados, énfasis, código, citas, listas y enlaces.
1406///
1407/// Es el que el operador nombró primero, y con razón: media documentación de
1408/// este repo es `.md`, y hasta acá se abría como un bloque de texto gris.
1409pub fn markdown(fuente: &str) -> Vec<Vec<Span>> {
1410    let mut out: Vec<Vec<Span>> = Vec::new();
1411    let mut en_valla = false;
1412    for linea in iterate_lines(fuente) {
1413        let ch: Vec<char> = linea.chars().collect();
1414        let len = ch.len();
1415        let mut spans: Vec<Span> = Vec::new();
1416        let ini = ch.iter().position(|c| !c.is_whitespace()).unwrap_or(len);
1417
1418        // Valla de código: la línea de la valla y todo lo de adentro, en verde.
1419        if empieza(&ch, ini, "```") || empieza(&ch, ini, "~~~") {
1420            en_valla = !en_valla;
1421            out.push(vec![Span { start_col: 0, end_col: len, kind: TokenKind::Comment }]);
1422            continue;
1423        }
1424        if en_valla {
1425            out.push(vec![Span { start_col: 0, end_col: len, kind: TokenKind::String }]);
1426            continue;
1427        }
1428        if ini >= len {
1429            out.push(fill_gaps(spans, len));
1430            continue;
1431        }
1432        // Encabezado.
1433        if ch[ini] == '#' {
1434            out.push(vec![Span { start_col: 0, end_col: len, kind: TokenKind::Keyword }]);
1435            continue;
1436        }
1437        // Cita.
1438        if ch[ini] == '>' {
1439            out.push(vec![Span { start_col: 0, end_col: len, kind: TokenKind::Comment }]);
1440            continue;
1441        }
1442        // Regla horizontal.
1443        if len - ini >= 3 && ch[ini..].iter().all(|c| *c == ch[ini] || c.is_whitespace())
1444            && matches!(ch[ini], '-' | '*' | '_' | '=')
1445        {
1446            out.push(vec![Span { start_col: 0, end_col: len, kind: TokenKind::Punctuation }]);
1447            continue;
1448        }
1449        // Marca de lista (`- `, `* `, `1. `).
1450        let mut i = ini;
1451        if (matches!(ch[i], '-' | '*' | '+') && i + 1 < len && ch[i + 1] == ' ')
1452            || (ch[i].is_ascii_digit() && marca_ordenada(&ch, i).is_some())
1453        {
1454            let fin = match marca_ordenada(&ch, i) {
1455                Some(f) => f,
1456                None => i + 1,
1457            };
1458            spans.push(Span { start_col: i, end_col: fin, kind: TokenKind::Punctuation });
1459            i = fin;
1460        }
1461        // Inline: código, énfasis, enlaces.
1462        spans.extend(inline_markdown(&ch, i));
1463        out.push(fill_gaps(spans, len));
1464    }
1465    out
1466}
1467
1468/// Fin de una marca de lista ordenada (`12. `), si la hay.
1469fn marca_ordenada(ch: &[char], i: usize) -> Option<usize> {
1470    let mut j = i;
1471    while j < ch.len() && ch[j].is_ascii_digit() {
1472        j += 1;
1473    }
1474    if j > i && j < ch.len() && (ch[j] == '.' || ch[j] == ')') {
1475        return Some(j + 1);
1476    }
1477    None
1478}
1479
1480fn inline_markdown(ch: &[char], desde: usize) -> Vec<Span> {
1481    let len = ch.len();
1482    let mut out = Vec::new();
1483    let mut i = desde;
1484    while i < len {
1485        let c = ch[i];
1486        // Código entre acentos graves.
1487        if c == '`' {
1488            match buscar(ch, i + 1, "`") {
1489                Some(f) => {
1490                    out.push(Span { start_col: i, end_col: f + 1, kind: TokenKind::String });
1491                    i = f + 1;
1492                    continue;
1493                }
1494                None => {
1495                    out.push(Span { start_col: i, end_col: len, kind: TokenKind::String });
1496                    break;
1497                }
1498            }
1499        }
1500        // Negrita / itálica.
1501        if c == '*' || c == '_' {
1502            let doble = i + 1 < len && ch[i + 1] == c;
1503            let marca: String = core::iter::repeat(c).take(if doble { 2 } else { 1 }).collect();
1504            if let Some(f) = buscar(ch, i + marca.chars().count(), &marca) {
1505                out.push(Span {
1506                    start_col: i,
1507                    end_col: f + marca.chars().count(),
1508                    kind: TokenKind::Type,
1509                });
1510                i = f + marca.chars().count();
1511                continue;
1512            }
1513        }
1514        // Enlace `[texto](destino)` o imagen `![texto](destino)`.
1515        if c == '[' {
1516            if let Some(cierre) = buscar(ch, i + 1, "]") {
1517                out.push(Span { start_col: i, end_col: cierre + 1, kind: TokenKind::Function });
1518                i = cierre + 1;
1519                if i < len && ch[i] == '(' {
1520                    let fin = buscar(ch, i, ")").map(|k| k + 1).unwrap_or(len);
1521                    out.push(Span { start_col: i, end_col: fin, kind: TokenKind::String });
1522                    i = fin;
1523                }
1524                continue;
1525            }
1526        }
1527        // Tabla.
1528        if c == '|' {
1529            out.push(Span { start_col: i, end_col: i + 1, kind: TokenKind::Punctuation });
1530        }
1531        i += 1;
1532    }
1533    out
1534}
1535
1536/// HTML / XML: etiquetas, atributos, valores, comentarios y entidades.
1537pub fn marcado(fuente: &str) -> Vec<Vec<Span>> {
1538    let mut out: Vec<Vec<Span>> = Vec::new();
1539    let mut en_comentario = false;
1540    let mut en_etiqueta = false;
1541    for linea in iterate_lines(fuente) {
1542        let ch: Vec<char> = linea.chars().collect();
1543        let len = ch.len();
1544        let mut spans: Vec<Span> = Vec::new();
1545        let mut i = 0usize;
1546        while i < len {
1547            if en_comentario {
1548                match buscar(&ch, i, "-->") {
1549                    Some(f) => {
1550                        spans.push(Span { start_col: i, end_col: f + 3, kind: TokenKind::Comment });
1551                        en_comentario = false;
1552                        i = f + 3;
1553                    }
1554                    None => {
1555                        spans.push(Span { start_col: i, end_col: len, kind: TokenKind::Comment });
1556                        i = len;
1557                    }
1558                }
1559                continue;
1560            }
1561            let c = ch[i];
1562            if !en_etiqueta {
1563                if empieza(&ch, i, "<!--") {
1564                    en_comentario = true;
1565                    continue;
1566                }
1567                if c == '<' {
1568                    let mut j = i + 1;
1569                    if j < len && (ch[j] == '/' || ch[j] == '!' || ch[j] == '?') {
1570                        j += 1;
1571                    }
1572                    let ini = j;
1573                    while j < len && (ch[j].is_alphanumeric() || ch[j] == '-' || ch[j] == ':' || ch[j] == '_') {
1574                        j += 1;
1575                    }
1576                    spans.push(Span { start_col: i, end_col: ini, kind: TokenKind::Punctuation });
1577                    if j > ini {
1578                        spans.push(Span { start_col: ini, end_col: j, kind: TokenKind::Keyword });
1579                    }
1580                    en_etiqueta = true;
1581                    i = j;
1582                    continue;
1583                }
1584                if c == '&' {
1585                    let fin = (i + 1..len.min(i + 12)).find(|&k| ch[k] == ';').map(|k| k + 1);
1586                    if let Some(f) = fin {
1587                        spans.push(Span { start_col: i, end_col: f, kind: TokenKind::Number });
1588                        i = f;
1589                        continue;
1590                    }
1591                }
1592                i += 1;
1593                continue;
1594            }
1595            // Adentro de una etiqueta: atributos y valores.
1596            if c == '>' {
1597                spans.push(Span { start_col: i, end_col: i + 1, kind: TokenKind::Punctuation });
1598                en_etiqueta = false;
1599                i += 1;
1600                continue;
1601            }
1602            if c == '"' || c == '\'' {
1603                let ini = i;
1604                i = fin_de_cadena(&ch, i, c);
1605                spans.push(Span { start_col: ini, end_col: i, kind: TokenKind::String });
1606                continue;
1607            }
1608            if c == '=' || c == '/' {
1609                spans.push(Span { start_col: i, end_col: i + 1, kind: TokenKind::Operator });
1610                i += 1;
1611                continue;
1612            }
1613            if c.is_alphabetic() || c == '_' {
1614                let ini = i;
1615                while i < len && (ch[i].is_alphanumeric() || ch[i] == '-' || ch[i] == ':' || ch[i] == '_') {
1616                    i += 1;
1617                }
1618                spans.push(Span { start_col: ini, end_col: i, kind: TokenKind::Function });
1619                continue;
1620            }
1621            i += 1;
1622        }
1623        out.push(fill_gaps(spans, len));
1624    }
1625    out
1626}
1627
1628/// CSS / SCSS: selectores, propiedades, valores, `@reglas` y comentarios.
1629pub fn css(fuente: &str) -> Vec<Vec<Span>> {
1630    let mut out: Vec<Vec<Span>> = Vec::new();
1631    let mut en_comentario = false;
1632    let mut en_bloque = false;
1633    for linea in iterate_lines(fuente) {
1634        let ch: Vec<char> = linea.chars().collect();
1635        let len = ch.len();
1636        let mut spans: Vec<Span> = Vec::new();
1637        let mut i = 0usize;
1638        while i < len {
1639            if en_comentario {
1640                match buscar(&ch, i, "*/") {
1641                    Some(f) => {
1642                        spans.push(Span { start_col: i, end_col: f + 2, kind: TokenKind::Comment });
1643                        en_comentario = false;
1644                        i = f + 2;
1645                    }
1646                    None => {
1647                        spans.push(Span { start_col: i, end_col: len, kind: TokenKind::Comment });
1648                        i = len;
1649                    }
1650                }
1651                continue;
1652            }
1653            if empieza(&ch, i, "/*") {
1654                en_comentario = true;
1655                continue;
1656            }
1657            if empieza(&ch, i, "//") {
1658                spans.push(Span { start_col: i, end_col: len, kind: TokenKind::Comment });
1659                break;
1660            }
1661            let c = ch[i];
1662            if c == '{' {
1663                en_bloque = true;
1664                spans.push(Span { start_col: i, end_col: i + 1, kind: TokenKind::Punctuation });
1665                i += 1;
1666                continue;
1667            }
1668            if c == '}' {
1669                en_bloque = false;
1670                spans.push(Span { start_col: i, end_col: i + 1, kind: TokenKind::Punctuation });
1671                i += 1;
1672                continue;
1673            }
1674            if c == '"' || c == '\'' {
1675                let ini = i;
1676                i = fin_de_cadena(&ch, i, c);
1677                spans.push(Span { start_col: ini, end_col: i, kind: TokenKind::String });
1678                continue;
1679            }
1680            if c == '@' {
1681                let ini = i;
1682                i += 1;
1683                while i < len && (ch[i].is_alphanumeric() || ch[i] == '-') {
1684                    i += 1;
1685                }
1686                spans.push(Span { start_col: ini, end_col: i, kind: TokenKind::Keyword });
1687                continue;
1688            }
1689            if c == '#' && i + 1 < len && ch[i + 1].is_ascii_hexdigit() && en_bloque {
1690                let ini = i;
1691                i += 1;
1692                while i < len && ch[i].is_ascii_hexdigit() {
1693                    i += 1;
1694                }
1695                spans.push(Span { start_col: ini, end_col: i, kind: TokenKind::Number });
1696                continue;
1697            }
1698            if c.is_ascii_digit() {
1699                let ini = i;
1700                while i < len && (ch[i].is_ascii_alphanumeric() || ch[i] == '.' || ch[i] == '%') {
1701                    i += 1;
1702                }
1703                spans.push(Span { start_col: ini, end_col: i, kind: TokenKind::Number });
1704                continue;
1705            }
1706            if c.is_alphabetic() || c == '-' || c == '.' || c == '#' || c == '$' {
1707                let ini = i;
1708                while i < len
1709                    && (ch[i].is_alphanumeric() || ch[i] == '-' || ch[i] == '_' || ch[i] == '.'
1710                        || ch[i] == '#' || ch[i] == '$')
1711                {
1712                    i += 1;
1713                }
1714                // Adentro de un bloque: lo que va antes del `:` es propiedad.
1715                let mut j = i;
1716                while j < len && ch[j] == ' ' {
1717                    j += 1;
1718                }
1719                let kind = if !en_bloque {
1720                    TokenKind::Type
1721                } else if j < len && ch[j] == ':' {
1722                    TokenKind::Function
1723                } else {
1724                    TokenKind::Identifier
1725                };
1726                spans.push(Span { start_col: ini, end_col: i, kind });
1727                continue;
1728            }
1729            if ":;,".contains(c) {
1730                spans.push(Span { start_col: i, end_col: i + 1, kind: TokenKind::Punctuation });
1731            }
1732            i += 1;
1733        }
1734        out.push(fill_gaps(spans, len));
1735    }
1736    out
1737}
1738
1739/// Diff / patch: `+` agregado, `-` borrado, `@@` tramo, cabeceras.
1740pub fn diff(fuente: &str) -> Vec<Vec<Span>> {
1741    let mut out: Vec<Vec<Span>> = Vec::new();
1742    for linea in iterate_lines(fuente) {
1743        let len = linea.chars().count();
1744        let kind = if linea.starts_with("+++") || linea.starts_with("---") {
1745            TokenKind::Keyword
1746        } else if linea.starts_with("@@") {
1747            TokenKind::Function
1748        } else if linea.starts_with('+') {
1749            TokenKind::String
1750        } else if linea.starts_with('-') {
1751            TokenKind::Number
1752        } else if linea.starts_with("diff ") || linea.starts_with("index ") {
1753            TokenKind::Type
1754        } else {
1755            TokenKind::Other
1756        };
1757        out.push(vec![Span { start_col: 0, end_col: len, kind }]);
1758    }
1759    out
1760}
1761
1762#[cfg(test)]
1763mod tests {
1764    use super::*;
1765
1766    fn kinds(spans: &[Span], texto: &str, trozo: &str) -> Option<TokenKind> {
1767        let ch: Vec<char> = texto.chars().collect();
1768        let col = texto.find(trozo).map(|b| texto[..b].chars().count())?;
1769        let _ = ch;
1770        spans.iter().find(|s| s.start_col == col).map(|s| s.kind)
1771    }
1772
1773    #[test]
1774    fn go_keyword_tipo_y_cadena() {
1775        let src = "func main() {\n\tvar x int = 3 // hola\n\ts := \"ho\"\n}";
1776        let r = tokenizar(src, &GO);
1777        assert_eq!(kinds(&r[0], "func main() {", "func"), Some(TokenKind::Keyword));
1778        assert_eq!(kinds(&r[0], "func main() {", "main"), Some(TokenKind::Function));
1779        assert!(r[1].iter().any(|s| s.kind == TokenKind::Type));
1780        assert!(r[1].iter().any(|s| s.kind == TokenKind::Comment));
1781        assert!(r[2].iter().any(|s| s.kind == TokenKind::String));
1782    }
1783
1784    #[test]
1785    fn comentario_de_bloque_cruza_renglones() {
1786        let src = "a /* uno\ndos */ b";
1787        let r = tokenizar(src, &C);
1788        assert!(r[0].iter().any(|s| s.kind == TokenKind::Comment));
1789        // La segunda línea abre con el comentario todavía abierto.
1790        assert_eq!(r[1][0].kind, TokenKind::Comment);
1791        assert!(r[1].iter().any(|s| s.kind == TokenKind::Identifier));
1792    }
1793
1794    #[test]
1795    fn python_cadena_triple_cruza_renglones() {
1796        let src = "s = \"\"\"uno\ndos\"\"\"\nx = 1";
1797        let r = tokenizar(src, &PYTHON);
1798        assert_eq!(r[1][0].kind, TokenKind::String);
1799        assert!(r[2].iter().any(|s| s.kind == TokenKind::Number));
1800    }
1801
1802    #[test]
1803    fn sql_ignora_la_caja() {
1804        let r = tokenizar("SELECT * FROM t WHERE a = 1", &SQL);
1805        assert_eq!(r[0][0].kind, TokenKind::Keyword);
1806        assert!(r[0].iter().any(|s| s.kind == TokenKind::Number));
1807    }
1808
1809    #[test]
1810    fn shell_comenta_con_almohadilla() {
1811        let r = tokenizar("cp a b # copia", &SHELL);
1812        assert!(r[0].iter().any(|s| s.kind == TokenKind::Comment));
1813        assert_eq!(r[0][0].kind, TokenKind::Type); // `cp` es un comando conocido
1814    }
1815
1816    #[test]
1817    fn json_distingue_clave_de_valor() {
1818        let r = json("{\"a\": \"b\"}");
1819        let f = r[0].iter().filter(|s| s.kind == TokenKind::Function).count();
1820        let s = r[0].iter().filter(|s| s.kind == TokenKind::String).count();
1821        assert_eq!((f, s), (1, 1));
1822    }
1823
1824    #[test]
1825    fn toml_seccion_clave_y_valor() {
1826        let r = toml("[paquete]\nnombre = \"nada\" # el editor\nedicion = 2021\n", false);
1827        assert_eq!(r[0][0].kind, TokenKind::Type);
1828        assert_eq!(r[1][0].kind, TokenKind::Function);
1829        assert!(r[1].iter().any(|s| s.kind == TokenKind::String));
1830        assert!(r[1].iter().any(|s| s.kind == TokenKind::Comment));
1831        assert!(r[2].iter().any(|s| s.kind == TokenKind::Number));
1832    }
1833
1834    #[test]
1835    fn yaml_clave_lista_y_comentario() {
1836        let r = yaml("clave: valor\n- uno\n# nota\n");
1837        assert_eq!(r[0][0].kind, TokenKind::Function);
1838        assert_eq!(r[1][0].kind, TokenKind::Punctuation);
1839        assert_eq!(r[2][0].kind, TokenKind::Comment);
1840    }
1841
1842    #[test]
1843    fn markdown_titulo_valla_y_enlace() {
1844        let r = markdown("# Título\n`code`\n```\nlo de adentro\n```\n[a](b)\n");
1845        assert_eq!(r[0][0].kind, TokenKind::Keyword);
1846        assert!(r[1].iter().any(|s| s.kind == TokenKind::String));
1847        assert_eq!(r[2][0].kind, TokenKind::Comment); // la valla
1848        assert_eq!(r[3][0].kind, TokenKind::String); // adentro de la valla
1849        assert!(r[5].iter().any(|s| s.kind == TokenKind::Function));
1850    }
1851
1852    #[test]
1853    fn html_etiqueta_atributo_y_valor() {
1854        let r = marcado("<a href=\"x\">t</a>");
1855        assert!(r[0].iter().any(|s| s.kind == TokenKind::Keyword));
1856        assert!(r[0].iter().any(|s| s.kind == TokenKind::Function));
1857        assert!(r[0].iter().any(|s| s.kind == TokenKind::String));
1858    }
1859
1860    #[test]
1861    fn css_selector_propiedad_y_numero() {
1862        let r = css(".a {\n  color: #fff;\n  ancho: 3px;\n}");
1863        assert_eq!(r[0][0].kind, TokenKind::Type);
1864        assert!(r[1].iter().any(|s| s.kind == TokenKind::Function));
1865        assert!(r[1].iter().any(|s| s.kind == TokenKind::Number));
1866        assert!(r[2].iter().any(|s| s.kind == TokenKind::Number));
1867    }
1868
1869    #[test]
1870    fn los_spans_cubren_toda_la_linea() {
1871        // El contrato de `highlight`: sin huecos ni solapes, o el renderer
1872        // pinta dos veces el mismo trozo.
1873        let casos: Vec<Vec<Vec<Span>>> = vec![
1874            tokenizar("fn a() { let b = \"c\"; }", &RUST),
1875            markdown("# t\n- [a](b) **x**\n"),
1876            yaml("a: 1\n"),
1877            toml("[a]\nb = 1\n", true),
1878            json("{\"a\": 1}"),
1879            css("a { b: 1px }"),
1880            marcado("<p class=\"x\">t</p>"),
1881        ];
1882        for lineas in casos {
1883            for spans in lineas {
1884                let mut cursor = 0usize;
1885                for s in &spans {
1886                    assert_eq!(s.start_col, cursor, "hueco o solape en {spans:?}");
1887                    cursor = s.end_col;
1888                }
1889            }
1890        }
1891    }
1892}