tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
chore: refresco desde el monorepo (pin tawasuyu.git v0.3.0)
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 ``.
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 **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}