arborium_theme/
highlights.rs

1//! Highlight category definitions - single source of truth.
2//!
3//! This module defines all highlight categories used for syntax highlighting.
4//! It maps the large vocabulary of capture names from various sources (nvim-treesitter,
5//! helix, etc.) to a small set of theme slots.
6//!
7//! # Architecture
8//!
9//! The highlighting system has three layers:
10//!
11//! 1. **Capture names** - The broad vocabulary used in highlight queries
12//!    (e.g., `@keyword.function`, `@include`, `@conditional`, `@repeat`)
13//!
14//! 2. **Theme slots** - A fixed set of ~15-20 color slots that themes define
15//!    (e.g., `keyword`, `function`, `string`, `comment`, `type`)
16//!
17//! 3. **HTML tags** - Short tags for rendering (e.g., `<a-k>`, `<a-f>`, `<a-s>`)
18//!
19//! Multiple capture names map to the same theme slot. For example:
20//! - `include`, `keyword.import`, `keyword.require` → all use the `keyword` slot
21//! - `conditional`, `keyword.conditional`, `repeat` → all use the `keyword` slot
22//!
23//! Adjacent spans that map to the same slot are coalesced into a single HTML element.
24
25/// The theme slots - the fixed set of color categories that themes define.
26/// This is the final destination for all capture names.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
28pub enum ThemeSlot {
29    Keyword,
30    Function,
31    String,
32    Comment,
33    Type,
34    Variable,
35    Constant,
36    Number,
37    Operator,
38    Punctuation,
39    Property,
40    Attribute,
41    Tag,
42    Macro,
43    Label,
44    Namespace,
45    Constructor,
46    /// Markup: headings, titles
47    Title,
48    /// Markup: bold text
49    Strong,
50    /// Markup: italic text
51    Emphasis,
52    /// Markup: links/URLs
53    Link,
54    /// Markup: raw/literal/code blocks
55    Literal,
56    /// Markup: strikethrough
57    Strikethrough,
58    /// Diff additions
59    DiffAdd,
60    /// Diff deletions
61    DiffDelete,
62    /// Embedded content
63    Embedded,
64    /// Errors
65    Error,
66    /// No styling (invisible captures like spell, nospell)
67    None,
68}
69
70impl ThemeSlot {
71    /// Get the HTML tag suffix for this slot.
72    /// Returns None for slots that produce no styling.
73    pub fn tag(self) -> Option<&'static str> {
74        match self {
75            ThemeSlot::Keyword => Some("k"),
76            ThemeSlot::Function => Some("f"),
77            ThemeSlot::String => Some("s"),
78            ThemeSlot::Comment => Some("c"),
79            ThemeSlot::Type => Some("t"),
80            ThemeSlot::Variable => Some("v"),
81            ThemeSlot::Constant => Some("co"),
82            ThemeSlot::Number => Some("n"),
83            ThemeSlot::Operator => Some("o"),
84            ThemeSlot::Punctuation => Some("p"),
85            ThemeSlot::Property => Some("pr"),
86            ThemeSlot::Attribute => Some("at"),
87            ThemeSlot::Tag => Some("tg"),
88            ThemeSlot::Macro => Some("m"),
89            ThemeSlot::Label => Some("l"),
90            ThemeSlot::Namespace => Some("ns"),
91            ThemeSlot::Constructor => Some("cr"),
92            ThemeSlot::Title => Some("tt"),
93            ThemeSlot::Strong => Some("st"),
94            ThemeSlot::Emphasis => Some("em"),
95            ThemeSlot::Link => Some("tu"),
96            ThemeSlot::Literal => Some("tl"),
97            ThemeSlot::Strikethrough => Some("tx"),
98            ThemeSlot::DiffAdd => Some("da"),
99            ThemeSlot::DiffDelete => Some("dd"),
100            ThemeSlot::Embedded => Some("eb"),
101            ThemeSlot::Error => Some("er"),
102            ThemeSlot::None => None,
103        }
104    }
105}
106
107/// Map any capture name to its theme slot.
108///
109/// This handles the full vocabulary of capture names from various sources:
110/// - Standard tree-sitter names (keyword, function, string, etc.)
111/// - nvim-treesitter names (include, conditional, repeat, storageclass, etc.)
112/// - Helix names
113/// - Sub-categories (keyword.function, keyword.import, etc.)
114///
115/// All are mapped to a fixed set of ~20 theme slots.
116pub fn capture_to_slot(capture: &str) -> ThemeSlot {
117    // First, strip any leading @ (some queries include it)
118    let capture = capture.strip_prefix('@').unwrap_or(capture);
119
120    match capture {
121        // Keywords - base and all variants
122        "keyword" | "keyword.conditional" | "keyword.coroutine" | "keyword.debug"
123        | "keyword.exception" | "keyword.function" | "keyword.import" | "keyword.operator"
124        | "keyword.repeat" | "keyword.return" | "keyword.type" | "keyword.modifier"
125        | "keyword.directive" | "keyword.storage" | "keyword.control"
126        | "keyword.control.conditional" | "keyword.control.repeat" | "keyword.control.import"
127        | "keyword.control.return" | "keyword.control.exception"
128        // nvim-treesitter legacy names that are really keywords
129        | "include" | "conditional" | "repeat" | "exception" | "storageclass" | "preproc"
130        | "define" | "structure" => ThemeSlot::Keyword,
131
132        // Functions
133        "function" | "function.builtin" | "function.method" | "function.definition"
134        | "function.call" | "function.special" | "method" | "method.call" => ThemeSlot::Function,
135
136        // Strings
137        "string" | "string.special" | "string.special.symbol" | "string.special.path"
138        | "string.special.url" | "string.escape" | "string.regexp" | "string.regex"
139        | "character" | "character.special" | "escape" => ThemeSlot::String,
140
141        // Comments
142        "comment" | "comment.documentation" | "comment.line" | "comment.block"
143        | "comment.error" | "comment.warning" | "comment.note" | "comment.todo" => {
144            ThemeSlot::Comment
145        }
146
147        // Types
148        "type" | "type.builtin" | "type.qualifier" | "type.definition" | "type.enum"
149        | "type.enum.variant" | "type.parameter" => ThemeSlot::Type,
150
151        // Variables
152        "variable" | "variable.builtin" | "variable.parameter" | "variable.member"
153        | "variable.other" | "variable.other.member" | "parameter" | "field" => {
154            ThemeSlot::Variable
155        }
156
157        // Constants
158        "constant" | "constant.builtin" | "constant.builtin.boolean" | "boolean" => {
159            ThemeSlot::Constant
160        }
161
162        // Numbers
163        "number" | "constant.numeric" | "float" | "number.float" => ThemeSlot::Number,
164
165        // Operators
166        "operator" => ThemeSlot::Operator,
167
168        // Punctuation
169        "punctuation" | "punctuation.bracket" | "punctuation.delimiter" | "punctuation.special" => {
170            ThemeSlot::Punctuation
171        }
172
173        // Properties
174        "property" | "property.builtin" => ThemeSlot::Property,
175
176        // Attributes
177        "attribute" | "attribute.builtin" => ThemeSlot::Attribute,
178
179        // Tags (HTML/XML)
180        "tag" | "tag.delimiter" | "tag.error" | "tag.attribute" | "tag.builtin" => ThemeSlot::Tag,
181
182        // Macros
183        "macro" | "function.macro" | "preproc.macro" => ThemeSlot::Macro,
184
185        // Labels
186        "label" => ThemeSlot::Label,
187
188        // Namespaces/Modules
189        "namespace" | "module" => ThemeSlot::Namespace,
190
191        // Constructors
192        "constructor" | "constructor.builtin" => ThemeSlot::Constructor,
193
194        // Markup - titles/headings
195        "text.title" | "markup.heading" | "markup.heading.1" | "markup.heading.2"
196        | "markup.heading.3" | "markup.heading.4" | "markup.heading.5" | "markup.heading.6" => {
197            ThemeSlot::Title
198        }
199
200        // Markup - bold
201        "text.strong" | "markup.bold" => ThemeSlot::Strong,
202
203        // Markup - italic
204        "text.emphasis" | "markup.italic" => ThemeSlot::Emphasis,
205
206        // Markup - links
207        "text.uri" | "text.reference" | "markup.link" | "markup.link.url" | "markup.link.text"
208        | "markup.link.label" => ThemeSlot::Link,
209
210        // Markup - literal/raw/code
211        "text.literal" | "markup.raw" | "markup.raw.block" | "markup.raw.inline"
212        | "markup.inline" => ThemeSlot::Literal,
213
214        // Markup - strikethrough
215        "text.strikethrough" | "markup.strikethrough" => ThemeSlot::Strikethrough,
216
217        // Markup - lists (treat as punctuation)
218        "markup.list" | "markup.list.checked" | "markup.list.unchecked"
219        | "markup.list.numbered" | "markup.list.unnumbered" | "markup.quote" => {
220            ThemeSlot::Punctuation
221        }
222
223        // Diff
224        "diff.addition" | "diff.plus" | "diff.delta" => ThemeSlot::DiffAdd,
225        "diff.deletion" | "diff.minus" => ThemeSlot::DiffDelete,
226
227        // Embedded
228        "embedded" => ThemeSlot::Embedded,
229
230        // Error
231        "error" => ThemeSlot::Error,
232
233        // No styling
234        "none" | "nospell" | "spell" | "text" | "markup" => ThemeSlot::None,
235
236        // Fallback: try to match by prefix
237        other => {
238            if other.starts_with("keyword") {
239                ThemeSlot::Keyword
240            } else if other.starts_with("function") || other.starts_with("method") {
241                ThemeSlot::Function
242            } else if other.starts_with("string") || other.starts_with("character") {
243                ThemeSlot::String
244            } else if other.starts_with("comment") {
245                ThemeSlot::Comment
246            } else if other.starts_with("type") {
247                ThemeSlot::Type
248            } else if other.starts_with("variable") || other.starts_with("parameter") {
249                ThemeSlot::Variable
250            } else if other.starts_with("constant") {
251                ThemeSlot::Constant
252            } else if other.starts_with("punctuation") {
253                ThemeSlot::Punctuation
254            } else if other.starts_with("tag") {
255                ThemeSlot::Tag
256            } else if other.starts_with("markup.heading") || other.starts_with("text.title") {
257                ThemeSlot::Title
258            } else if other.starts_with("markup") || other.starts_with("text") {
259                // Generic markup/text - no styling
260                ThemeSlot::None
261            } else {
262                // Unknown capture - no styling
263                ThemeSlot::None
264            }
265        }
266    }
267}
268
269/// A highlight category definition.
270///
271/// NOTE: This is the legacy structure used for tree-sitter highlight configuration.
272/// For mapping captures to theme slots, use [`capture_to_slot`] instead.
273pub struct HighlightDef {
274    /// The canonical name (e.g., "keyword.function")
275    pub name: &'static str,
276    /// Short tag suffix for HTML elements (e.g., "kf" -> `<a-kf>`)
277    /// Empty string means no styling should be applied.
278    pub tag: &'static str,
279    /// Parent category tag for CSS inheritance (e.g., "k" for keyword.*)
280    /// Empty string means no parent.
281    pub parent_tag: &'static str,
282    /// Alternative names from nvim-treesitter/helix/other editors that map to this category
283    pub aliases: &'static [&'static str],
284}
285
286/// All highlight categories, in order.
287/// The index in this array is the highlight index used throughout the codebase.
288pub const HIGHLIGHTS: &[HighlightDef] = &[
289    // Core categories
290    HighlightDef {
291        name: "attribute",
292        tag: "at",
293        parent_tag: "",
294        aliases: &[],
295    },
296    HighlightDef {
297        name: "constant",
298        tag: "co",
299        parent_tag: "",
300        aliases: &[],
301    },
302    HighlightDef {
303        name: "constant.builtin",
304        tag: "cb",
305        parent_tag: "co",
306        aliases: &["constant.builtin.boolean"],
307    },
308    HighlightDef {
309        name: "constructor",
310        tag: "cr",
311        parent_tag: "",
312        aliases: &[],
313    },
314    HighlightDef {
315        name: "function.builtin",
316        tag: "fb",
317        parent_tag: "f",
318        aliases: &[],
319    },
320    HighlightDef {
321        name: "function",
322        tag: "f",
323        parent_tag: "",
324        aliases: &[],
325    },
326    HighlightDef {
327        name: "function.method",
328        tag: "fm",
329        parent_tag: "f",
330        aliases: &[],
331    },
332    HighlightDef {
333        name: "keyword",
334        tag: "k",
335        parent_tag: "",
336        aliases: &[],
337    },
338    HighlightDef {
339        name: "keyword.conditional",
340        tag: "kc",
341        parent_tag: "k",
342        aliases: &[],
343    },
344    HighlightDef {
345        name: "keyword.coroutine",
346        tag: "ko",
347        parent_tag: "k",
348        aliases: &[],
349    },
350    HighlightDef {
351        name: "keyword.debug",
352        tag: "kd",
353        parent_tag: "k",
354        aliases: &[],
355    },
356    HighlightDef {
357        name: "keyword.exception",
358        tag: "ke",
359        parent_tag: "k",
360        aliases: &[],
361    },
362    HighlightDef {
363        name: "keyword.function",
364        tag: "kf",
365        parent_tag: "k",
366        aliases: &[],
367    },
368    HighlightDef {
369        name: "keyword.import",
370        tag: "ki",
371        parent_tag: "k",
372        aliases: &[],
373    },
374    HighlightDef {
375        name: "keyword.operator",
376        tag: "kp",
377        parent_tag: "k",
378        aliases: &[],
379    },
380    HighlightDef {
381        name: "keyword.repeat",
382        tag: "kr",
383        parent_tag: "k",
384        aliases: &[],
385    },
386    HighlightDef {
387        name: "keyword.return",
388        tag: "kt",
389        parent_tag: "k",
390        aliases: &[],
391    },
392    HighlightDef {
393        name: "keyword.type",
394        tag: "ky",
395        parent_tag: "k",
396        aliases: &[],
397    },
398    HighlightDef {
399        name: "operator",
400        tag: "o",
401        parent_tag: "",
402        aliases: &[],
403    },
404    HighlightDef {
405        name: "property",
406        tag: "pr",
407        parent_tag: "",
408        aliases: &[],
409    },
410    HighlightDef {
411        name: "punctuation",
412        tag: "p",
413        parent_tag: "",
414        aliases: &[],
415    },
416    HighlightDef {
417        name: "punctuation.bracket",
418        tag: "pb",
419        parent_tag: "p",
420        aliases: &[],
421    },
422    HighlightDef {
423        name: "punctuation.delimiter",
424        tag: "pd",
425        parent_tag: "p",
426        aliases: &[],
427    },
428    HighlightDef {
429        name: "punctuation.special",
430        tag: "ps",
431        parent_tag: "p",
432        aliases: &[],
433    },
434    HighlightDef {
435        name: "string",
436        tag: "s",
437        parent_tag: "",
438        aliases: &[],
439    },
440    HighlightDef {
441        name: "string.special",
442        tag: "ss",
443        parent_tag: "s",
444        aliases: &["string.special.symbol", "string.special.path"],
445    },
446    HighlightDef {
447        name: "tag",
448        tag: "tg",
449        parent_tag: "",
450        aliases: &[],
451    },
452    HighlightDef {
453        name: "tag.delimiter",
454        tag: "td",
455        parent_tag: "tg",
456        aliases: &[],
457    },
458    HighlightDef {
459        name: "tag.error",
460        tag: "te",
461        parent_tag: "tg",
462        aliases: &[],
463    },
464    HighlightDef {
465        name: "type",
466        tag: "t",
467        parent_tag: "",
468        aliases: &[],
469    },
470    HighlightDef {
471        name: "type.builtin",
472        tag: "tb",
473        parent_tag: "t",
474        aliases: &[],
475    },
476    HighlightDef {
477        name: "type.qualifier",
478        tag: "tq",
479        parent_tag: "t",
480        aliases: &[],
481    },
482    HighlightDef {
483        name: "variable",
484        tag: "v",
485        parent_tag: "",
486        aliases: &[],
487    },
488    HighlightDef {
489        name: "variable.builtin",
490        tag: "vb",
491        parent_tag: "v",
492        aliases: &[],
493    },
494    HighlightDef {
495        name: "variable.parameter",
496        tag: "vp",
497        parent_tag: "v",
498        aliases: &["parameter"],
499    },
500    HighlightDef {
501        name: "comment",
502        tag: "c",
503        parent_tag: "",
504        aliases: &[],
505    },
506    HighlightDef {
507        name: "comment.documentation",
508        tag: "cd",
509        parent_tag: "c",
510        aliases: &[],
511    },
512    HighlightDef {
513        name: "macro",
514        tag: "m",
515        parent_tag: "",
516        aliases: &[],
517    },
518    HighlightDef {
519        name: "label",
520        tag: "l",
521        parent_tag: "",
522        aliases: &[],
523    },
524    HighlightDef {
525        name: "diff.addition",
526        tag: "da",
527        parent_tag: "",
528        aliases: &["diff.plus", "diff.delta"],
529    },
530    HighlightDef {
531        name: "diff.deletion",
532        tag: "dd",
533        parent_tag: "",
534        aliases: &["diff.minus"],
535    },
536    HighlightDef {
537        name: "number",
538        tag: "n",
539        parent_tag: "",
540        aliases: &["constant.numeric"],
541    },
542    HighlightDef {
543        name: "text.literal",
544        tag: "tl",
545        parent_tag: "",
546        aliases: &["markup.raw"],
547    },
548    HighlightDef {
549        name: "text.emphasis",
550        tag: "em",
551        parent_tag: "",
552        aliases: &["markup.italic"],
553    },
554    HighlightDef {
555        name: "text.strong",
556        tag: "st",
557        parent_tag: "",
558        aliases: &["markup.bold"],
559    },
560    HighlightDef {
561        name: "text.uri",
562        tag: "tu",
563        parent_tag: "",
564        aliases: &["markup.link.url"],
565    },
566    HighlightDef {
567        name: "text.reference",
568        tag: "tr",
569        parent_tag: "",
570        aliases: &["markup.link.text"],
571    },
572    HighlightDef {
573        name: "string.escape",
574        tag: "se",
575        parent_tag: "s",
576        aliases: &["escape"],
577    },
578    HighlightDef {
579        name: "text.title",
580        tag: "tt",
581        parent_tag: "",
582        aliases: &["markup.heading"],
583    },
584    HighlightDef {
585        name: "text.strikethrough",
586        tag: "tx",
587        parent_tag: "",
588        aliases: &["markup.strikethrough"],
589    },
590    HighlightDef {
591        name: "spell",
592        tag: "sp",
593        parent_tag: "",
594        aliases: &[],
595    },
596    HighlightDef {
597        name: "embedded",
598        tag: "eb",
599        parent_tag: "",
600        aliases: &[],
601    },
602    HighlightDef {
603        name: "error",
604        tag: "er",
605        parent_tag: "",
606        aliases: &[],
607    },
608    HighlightDef {
609        name: "namespace",
610        tag: "ns",
611        parent_tag: "",
612        aliases: &["module"],
613    },
614    // Legacy/alternative names used by some grammars
615    HighlightDef {
616        name: "include",
617        tag: "in",
618        parent_tag: "k",
619        aliases: &[],
620    },
621    HighlightDef {
622        name: "storageclass",
623        tag: "sc",
624        parent_tag: "k",
625        aliases: &[],
626    },
627    HighlightDef {
628        name: "repeat",
629        tag: "rp",
630        parent_tag: "k",
631        aliases: &[],
632    },
633    HighlightDef {
634        name: "conditional",
635        tag: "cn",
636        parent_tag: "k",
637        aliases: &[],
638    },
639    HighlightDef {
640        name: "exception",
641        tag: "ex",
642        parent_tag: "k",
643        aliases: &[],
644    },
645    HighlightDef {
646        name: "preproc",
647        tag: "pp",
648        parent_tag: "k",
649        aliases: &[],
650    },
651    HighlightDef {
652        name: "none",
653        tag: "",
654        parent_tag: "",
655        aliases: &[],
656    }, // No styling
657    HighlightDef {
658        name: "character",
659        tag: "ch",
660        parent_tag: "s",
661        aliases: &[],
662    },
663    HighlightDef {
664        name: "character.special",
665        tag: "cs",
666        parent_tag: "s",
667        aliases: &[],
668    },
669    HighlightDef {
670        name: "variable.member",
671        tag: "vm",
672        parent_tag: "v",
673        aliases: &[],
674    },
675    HighlightDef {
676        name: "function.definition",
677        tag: "fd",
678        parent_tag: "f",
679        aliases: &[],
680    },
681    HighlightDef {
682        name: "type.definition",
683        tag: "tf",
684        parent_tag: "t",
685        aliases: &[],
686    },
687    HighlightDef {
688        name: "function.call",
689        tag: "fc",
690        parent_tag: "f",
691        aliases: &[],
692    },
693    HighlightDef {
694        name: "keyword.modifier",
695        tag: "km",
696        parent_tag: "k",
697        aliases: &[],
698    },
699    HighlightDef {
700        name: "keyword.directive",
701        tag: "dr",
702        parent_tag: "k",
703        aliases: &[],
704    },
705    HighlightDef {
706        name: "string.regexp",
707        tag: "rx",
708        parent_tag: "s",
709        aliases: &["string.regex"],
710    },
711    HighlightDef {
712        name: "nospell",
713        tag: "",
714        parent_tag: "",
715        aliases: &[],
716    }, // No styling
717    HighlightDef {
718        name: "float",
719        tag: "n",
720        parent_tag: "",
721        aliases: &[],
722    }, // Same as number
723    HighlightDef {
724        name: "boolean",
725        tag: "cb",
726        parent_tag: "",
727        aliases: &[],
728    }, // Same as constant.builtin
729];
730
731/// Get the highlight names array for tree-sitter configuration.
732pub const fn names() -> [&'static str; HIGHLIGHTS.len()] {
733    let mut names = [""; HIGHLIGHTS.len()];
734    let mut i = 0;
735    while i < HIGHLIGHTS.len() {
736        names[i] = HIGHLIGHTS[i].name;
737        i += 1;
738    }
739    names
740}
741
742/// Total number of highlight categories.
743pub const COUNT: usize = HIGHLIGHTS.len();
744
745/// Get the HTML tag suffix for a highlight index.
746/// Returns None for indices that should produce no styling (like "none" or "nospell").
747#[inline]
748pub fn tag(index: usize) -> Option<&'static str> {
749    HIGHLIGHTS
750        .get(index)
751        .map(|h| h.tag)
752        .filter(|t| !t.is_empty())
753}
754
755/// Get the prefixed HTML tag (e.g., "a-kf") for a highlight index.
756#[inline]
757pub fn prefixed_tag(index: usize) -> Option<String> {
758    tag(index).map(|t| format!("a-{t}"))
759}
760
761/// Get the parent tag for inheritance, if any.
762#[inline]
763pub fn parent_tag(index: usize) -> Option<&'static str> {
764    HIGHLIGHTS
765        .get(index)
766        .map(|h| h.parent_tag)
767        .filter(|t| !t.is_empty())
768}
769
770/// Generate CSS inheritance rules for sub-categories.
771/// Returns rules like "a-kc, a-kf, a-ki { color: inherit; }" grouped by parent.
772pub fn css_inheritance_rules() -> String {
773    use std::collections::HashMap;
774    use std::fmt::Write;
775
776    // Group children by parent
777    let mut parent_children: HashMap<&str, Vec<&str>> = HashMap::new();
778    for def in HIGHLIGHTS {
779        if !def.parent_tag.is_empty() && !def.tag.is_empty() {
780            parent_children
781                .entry(def.parent_tag)
782                .or_default()
783                .push(def.tag);
784        }
785    }
786
787    let mut css = String::new();
788    for (_parent, children) in parent_children {
789        if children.is_empty() {
790            continue;
791        }
792        // Create selector list: a-kc, a-kf, a-ki, ...
793        let selectors: Vec<String> = children.iter().map(|c| format!("a-{c}")).collect();
794        writeln!(css, "{} {{ color: inherit; }}", selectors.join(", ")).unwrap();
795    }
796    css
797}
798
799/// Get the HTML tag for a capture name directly.
800///
801/// This is the main function to use when rendering HTML from captures.
802/// It maps any capture name to its theme slot and returns the tag.
803///
804/// Returns None for captures that should produce no styling.
805///
806/// # Example
807/// ```
808/// use arborium_theme::highlights::tag_for_capture;
809///
810/// // All these map to the keyword slot ("k")
811/// assert_eq!(tag_for_capture("keyword"), Some("k"));
812/// assert_eq!(tag_for_capture("keyword.function"), Some("k"));
813/// assert_eq!(tag_for_capture("include"), Some("k"));
814/// assert_eq!(tag_for_capture("conditional"), Some("k"));
815///
816/// // No styling for these
817/// assert_eq!(tag_for_capture("spell"), None);
818/// assert_eq!(tag_for_capture("nospell"), None);
819/// ```
820pub fn tag_for_capture(capture: &str) -> Option<&'static str> {
821    capture_to_slot(capture).tag()
822}
823
824/// The complete list of capture names that arborium recognizes.
825///
826/// This list is used to configure tree-sitter's highlight query processor.
827/// It includes all standard names plus common alternatives from nvim-treesitter,
828/// helix, and other editors.
829pub const CAPTURE_NAMES: &[&str] = &[
830    // Keywords
831    "keyword",
832    "keyword.conditional",
833    "keyword.coroutine",
834    "keyword.debug",
835    "keyword.exception",
836    "keyword.function",
837    "keyword.import",
838    "keyword.operator",
839    "keyword.repeat",
840    "keyword.return",
841    "keyword.type",
842    "keyword.modifier",
843    "keyword.directive",
844    "keyword.storage",
845    "keyword.control",
846    "keyword.control.conditional",
847    "keyword.control.repeat",
848    "keyword.control.import",
849    "keyword.control.return",
850    "keyword.control.exception",
851    // nvim-treesitter legacy keyword names
852    "include",
853    "conditional",
854    "repeat",
855    "exception",
856    "storageclass",
857    "preproc",
858    "define",
859    "structure",
860    // Functions
861    "function",
862    "function.builtin",
863    "function.method",
864    "function.definition",
865    "function.call",
866    "function.macro",
867    "function.special",
868    "method",
869    "method.call",
870    // Strings
871    "string",
872    "string.special",
873    "string.special.symbol",
874    "string.special.path",
875    "string.special.url",
876    "string.escape",
877    "string.regexp",
878    "string.regex",
879    "character",
880    "character.special",
881    "escape",
882    // Comments
883    "comment",
884    "comment.documentation",
885    "comment.line",
886    "comment.block",
887    "comment.error",
888    "comment.warning",
889    "comment.note",
890    "comment.todo",
891    // Types
892    "type",
893    "type.builtin",
894    "type.qualifier",
895    "type.definition",
896    "type.enum",
897    "type.enum.variant",
898    "type.parameter",
899    // Variables
900    "variable",
901    "variable.builtin",
902    "variable.parameter",
903    "variable.member",
904    "variable.other",
905    "variable.other.member",
906    "parameter",
907    "field",
908    // Constants
909    "constant",
910    "constant.builtin",
911    "constant.builtin.boolean",
912    "constant.numeric",
913    "boolean",
914    // Numbers
915    "number",
916    "float",
917    "number.float",
918    // Operators
919    "operator",
920    // Punctuation
921    "punctuation",
922    "punctuation.bracket",
923    "punctuation.delimiter",
924    "punctuation.special",
925    // Properties
926    "property",
927    "property.builtin",
928    // Attributes
929    "attribute",
930    "attribute.builtin",
931    // Tags
932    "tag",
933    "tag.delimiter",
934    "tag.error",
935    "tag.attribute",
936    "tag.builtin",
937    // Macros
938    "macro",
939    // Labels
940    "label",
941    // Namespaces
942    "namespace",
943    "module",
944    // Constructors
945    "constructor",
946    "constructor.builtin",
947    // Markup - titles
948    "text.title",
949    "markup.heading",
950    "markup.heading.1",
951    "markup.heading.2",
952    "markup.heading.3",
953    "markup.heading.4",
954    "markup.heading.5",
955    "markup.heading.6",
956    // Markup - emphasis
957    "text.strong",
958    "markup.bold",
959    "text.emphasis",
960    "markup.italic",
961    // Markup - links
962    "text.uri",
963    "text.reference",
964    "markup.link",
965    "markup.link.url",
966    "markup.link.text",
967    "markup.link.label",
968    // Markup - code/raw
969    "text.literal",
970    "markup.raw",
971    "markup.raw.block",
972    "markup.raw.inline",
973    "markup.inline",
974    // Markup - strikethrough
975    "text.strikethrough",
976    "markup.strikethrough",
977    // Markup - lists
978    "markup.list",
979    "markup.list.checked",
980    "markup.list.unchecked",
981    "markup.list.numbered",
982    "markup.list.unnumbered",
983    "markup.quote",
984    // Markup - generic
985    "text",
986    "markup",
987    // Diff
988    "diff.addition",
989    "diff.plus",
990    "diff.delta",
991    "diff.deletion",
992    "diff.minus",
993    // Special
994    "embedded",
995    "error",
996    "none",
997    "nospell",
998    "spell",
999];
1000
1001#[cfg(test)]
1002mod tests {
1003    use super::*;
1004
1005    #[test]
1006    fn test_names_count() {
1007        assert_eq!(names().len(), COUNT);
1008    }
1009
1010    #[test]
1011    fn test_none_produces_no_tag() {
1012        // Find the "none" index
1013        let none_idx = HIGHLIGHTS.iter().position(|h| h.name == "none").unwrap();
1014        assert_eq!(tag(none_idx), None);
1015    }
1016
1017    #[test]
1018    fn test_keyword_tag() {
1019        let kw_idx = HIGHLIGHTS.iter().position(|h| h.name == "keyword").unwrap();
1020        assert_eq!(tag(kw_idx), Some("k"));
1021        assert_eq!(prefixed_tag(kw_idx), Some("a-k".to_string()));
1022    }
1023
1024    #[test]
1025    fn test_inheritance() {
1026        let kc_idx = HIGHLIGHTS
1027            .iter()
1028            .position(|h| h.name == "keyword.conditional")
1029            .unwrap();
1030        assert_eq!(parent_tag(kc_idx), Some("k"));
1031    }
1032
1033    #[test]
1034    fn test_capture_to_slot_keywords() {
1035        // All keyword variants map to Keyword slot
1036        assert_eq!(capture_to_slot("keyword"), ThemeSlot::Keyword);
1037        assert_eq!(capture_to_slot("keyword.function"), ThemeSlot::Keyword);
1038        assert_eq!(capture_to_slot("keyword.import"), ThemeSlot::Keyword);
1039        assert_eq!(capture_to_slot("include"), ThemeSlot::Keyword);
1040        assert_eq!(capture_to_slot("conditional"), ThemeSlot::Keyword);
1041        assert_eq!(capture_to_slot("repeat"), ThemeSlot::Keyword);
1042        assert_eq!(capture_to_slot("storageclass"), ThemeSlot::Keyword);
1043    }
1044
1045    #[test]
1046    fn test_capture_to_slot_functions() {
1047        assert_eq!(capture_to_slot("function"), ThemeSlot::Function);
1048        assert_eq!(capture_to_slot("function.builtin"), ThemeSlot::Function);
1049        assert_eq!(capture_to_slot("function.method"), ThemeSlot::Function);
1050        assert_eq!(capture_to_slot("method"), ThemeSlot::Function);
1051    }
1052
1053    #[test]
1054    fn test_capture_to_slot_markup() {
1055        assert_eq!(capture_to_slot("markup.heading"), ThemeSlot::Title);
1056        assert_eq!(capture_to_slot("markup.heading.1"), ThemeSlot::Title);
1057        assert_eq!(capture_to_slot("text.title"), ThemeSlot::Title);
1058        assert_eq!(capture_to_slot("markup.bold"), ThemeSlot::Strong);
1059        assert_eq!(capture_to_slot("markup.italic"), ThemeSlot::Emphasis);
1060    }
1061
1062    #[test]
1063    fn test_capture_to_slot_none() {
1064        assert_eq!(capture_to_slot("none"), ThemeSlot::None);
1065        assert_eq!(capture_to_slot("spell"), ThemeSlot::None);
1066        assert_eq!(capture_to_slot("nospell"), ThemeSlot::None);
1067    }
1068
1069    #[test]
1070    fn test_tag_for_capture() {
1071        // Keywords all get "k"
1072        assert_eq!(tag_for_capture("keyword"), Some("k"));
1073        assert_eq!(tag_for_capture("keyword.function"), Some("k"));
1074        assert_eq!(tag_for_capture("include"), Some("k"));
1075        assert_eq!(tag_for_capture("conditional"), Some("k"));
1076
1077        // Functions get "f"
1078        assert_eq!(tag_for_capture("function"), Some("f"));
1079        assert_eq!(tag_for_capture("function.builtin"), Some("f"));
1080
1081        // Comments get "c"
1082        assert_eq!(tag_for_capture("comment"), Some("c"));
1083        assert_eq!(tag_for_capture("comment.documentation"), Some("c"));
1084
1085        // No tag for special captures
1086        assert_eq!(tag_for_capture("spell"), None);
1087        assert_eq!(tag_for_capture("none"), None);
1088    }
1089
1090    #[test]
1091    fn test_theme_slot_tag() {
1092        assert_eq!(ThemeSlot::Keyword.tag(), Some("k"));
1093        assert_eq!(ThemeSlot::Function.tag(), Some("f"));
1094        assert_eq!(ThemeSlot::String.tag(), Some("s"));
1095        assert_eq!(ThemeSlot::Comment.tag(), Some("c"));
1096        assert_eq!(ThemeSlot::None.tag(), None);
1097    }
1098
1099    #[test]
1100    fn test_capture_names_all_map_to_slot() {
1101        // Every name in CAPTURE_NAMES should produce a valid mapping
1102        for name in CAPTURE_NAMES {
1103            let slot = capture_to_slot(name);
1104            // Just verify it doesn't panic and produces some slot
1105            let _ = slot.tag();
1106        }
1107    }
1108}