fabrica

hanna/fabrica

3551 bytes
Raw
1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! The one table mapping tree-sitter capture names onto fabrica's stable, themeable
6//! CSS class names.
7//!
8//! Themes style code by these class names alone and never need to know tree-sitter
9//! internals; the full list is documented in `docs/theming.md`. Capture names are
10//! hierarchical and dotted (`keyword.control.return`, `constant.numeric.integer`);
11//! we map on the first component, with `constant.numeric*` special-cased to the
12//! number class. Captures with no mapping render unhighlighted (no wrapping span).
13
14/// The complete set of highlight classes a theme may style. Kept in sync with the
15/// `--fb-hl-*` token family in `docs/theming.md`.
16pub const HL_CLASSES: &[&str] = &[
17 "hl-keyword",
18 "hl-function",
19 "hl-type",
20 "hl-string",
21 "hl-number",
22 "hl-comment",
23 "hl-constant",
24 "hl-variable",
25 "hl-operator",
26 "hl-punctuation",
27 "hl-attribute",
28 "hl-tag",
29];
30
31/// Map a tree-sitter capture name to a fabrica highlight class, or `None` when the
32/// capture has no styling (it renders as plain text within its line).
33#[must_use]
34pub fn class_for(capture: &str) -> Option<&'static str> {
35 // `constant.numeric.*` is a constant in the grammar but a number to us.
36 if capture.starts_with("constant.numeric") {
37 return Some("hl-number");
38 }
39 let head = capture.split('.').next().unwrap_or(capture);
40 Some(match head {
41 "keyword" | "label" => "hl-keyword",
42 "function" | "method" => "hl-function",
43 "type" | "constructor" | "namespace" => "hl-type",
44 "string" | "escape" | "char" | "regex" => "hl-string",
45 "number" | "float" | "boolean" => "hl-number",
46 "comment" => "hl-comment",
47 "constant" => "hl-constant",
48 "variable" | "property" | "parameter" | "field" => "hl-variable",
49 "operator" => "hl-operator",
50 "punctuation" => "hl-punctuation",
51 "attribute" | "annotation" | "decorator" => "hl-attribute",
52 "tag" => "hl-tag",
53 _ => return None,
54 })
55}
56
57#[cfg(test)]
58mod tests {
59 #![allow(clippy::unwrap_used)]
60
61 use super::*;
62
63 #[test]
64 fn maps_common_captures() {
65 assert_eq!(class_for("keyword"), Some("hl-keyword"));
66 assert_eq!(class_for("keyword.control.return"), Some("hl-keyword"));
67 assert_eq!(class_for("function.macro"), Some("hl-function"));
68 assert_eq!(class_for("type.builtin"), Some("hl-type"));
69 assert_eq!(class_for("string"), Some("hl-string"));
70 assert_eq!(class_for("constant.numeric.integer"), Some("hl-number"));
71 assert_eq!(class_for("constant.builtin"), Some("hl-constant"));
72 assert_eq!(class_for("punctuation.delimiter"), Some("hl-punctuation"));
73 assert_eq!(class_for("variable.parameter"), Some("hl-variable"));
74 }
75
76 #[test]
77 fn unmapped_captures_are_none() {
78 assert_eq!(class_for("markup.heading"), None);
79 assert_eq!(class_for("diff.plus"), None);
80 assert_eq!(class_for("something.unknown"), None);
81 }
82
83 #[test]
84 fn every_class_is_documented_in_the_list() {
85 for capture in ["keyword", "function", "type", "string", "comment"] {
86 let class = class_for(capture).unwrap();
87 assert!(
88 HL_CLASSES.contains(&class),
89 "{class} missing from HL_CLASSES"
90 );
91 }
92 }
93}