// This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. //! The one table mapping tree-sitter capture names onto fabrica's stable, themeable //! CSS class names. //! //! Themes style code by these class names alone and never need to know tree-sitter //! internals; the full list is documented in `docs/theming.md`. Capture names are //! hierarchical and dotted (`keyword.control.return`, `constant.numeric.integer`); //! we map on the first component, with `constant.numeric*` special-cased to the //! number class. Captures with no mapping render unhighlighted (no wrapping span). /// The complete set of highlight classes a theme may style. Kept in sync with the /// `--fb-hl-*` token family in `docs/theming.md`. pub const HL_CLASSES: &[&str] = &[ "hl-keyword", "hl-function", "hl-type", "hl-string", "hl-number", "hl-comment", "hl-constant", "hl-variable", "hl-operator", "hl-punctuation", "hl-attribute", "hl-tag", ]; /// Map a tree-sitter capture name to a fabrica highlight class, or `None` when the /// capture has no styling (it renders as plain text within its line). #[must_use] pub fn class_for(capture: &str) -> Option<&'static str> { // `constant.numeric.*` is a constant in the grammar but a number to us. if capture.starts_with("constant.numeric") { return Some("hl-number"); } let head = capture.split('.').next().unwrap_or(capture); Some(match head { "keyword" | "label" => "hl-keyword", "function" | "method" => "hl-function", "type" | "constructor" | "namespace" => "hl-type", "string" | "escape" | "char" | "regex" => "hl-string", "number" | "float" | "boolean" => "hl-number", "comment" => "hl-comment", "constant" => "hl-constant", "variable" | "property" | "parameter" | "field" => "hl-variable", "operator" => "hl-operator", "punctuation" => "hl-punctuation", "attribute" | "annotation" | "decorator" => "hl-attribute", "tag" => "hl-tag", _ => return None, }) } #[cfg(test)] mod tests { #![allow(clippy::unwrap_used)] use super::*; #[test] fn maps_common_captures() { assert_eq!(class_for("keyword"), Some("hl-keyword")); assert_eq!(class_for("keyword.control.return"), Some("hl-keyword")); assert_eq!(class_for("function.macro"), Some("hl-function")); assert_eq!(class_for("type.builtin"), Some("hl-type")); assert_eq!(class_for("string"), Some("hl-string")); assert_eq!(class_for("constant.numeric.integer"), Some("hl-number")); assert_eq!(class_for("constant.builtin"), Some("hl-constant")); assert_eq!(class_for("punctuation.delimiter"), Some("hl-punctuation")); assert_eq!(class_for("variable.parameter"), Some("hl-variable")); } #[test] fn unmapped_captures_are_none() { assert_eq!(class_for("markup.heading"), None); assert_eq!(class_for("diff.plus"), None); assert_eq!(class_for("something.unknown"), None); } #[test] fn every_class_is_documented_in_the_list() { for capture in ["keyword", "function", "type", "string", "comment"] { let class = class_for(capture).unwrap(); assert!( HL_CLASSES.contains(&class), "{class} missing from HL_CLASSES" ); } } }