Skip to main content

hikari_token/
lib.rs

1//! hikari (光) — the one fleet-facing semantic highlight vocabulary.
2//!
3//! The pleme-io fleet grew several byte-identical copies of a 16-variant
4//! highlight-class enum (`escriba_ts::Semantic`, `caixa_theme::Semantic`, …).
5//! This crate owns the single canonical definition — [`Semantic`] — plus a
6//! **total** morphism to/from [`hikari_core::HlClass`] (the palette-independent
7//! class the highlighter backends emit). Consumers re-export [`Semantic`] from
8//! here and delete their local copy, so the vocabulary lives in one place and
9//! every consumer inherits changes on the next dep bump.
10//!
11//! A consumer takes ONE dependency: [`HlClass`], [`ByteSpan`], and
12//! [`HighlightSpan`] are re-exported.
13
14#![forbid(unsafe_code)]
15
16pub use hikari_core::{ByteSpan, HighlightSpan, HlClass};
17
18use serde::{Deserialize, Serialize};
19
20/// The fleet semantic highlight class — the theme-facing vocabulary a renderer
21/// maps to color. Variant set + **order** are load-bearing: they are
22/// byte-identical to the historical `escriba_ts::Semantic` /
23/// `caixa_theme::Semantic` so serde / `JsonSchema` output does not drift when
24/// those crates re-export this one.
25#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Serialize, Deserialize)]
26#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
27#[serde(rename_all = "snake_case")]
28pub enum Semantic {
29    Keyword,
30    Symbol,
31    KeywordArg,
32    String,
33    Number,
34    Literal,
35    Comment,
36    Accent,
37    Muted,
38    Error,
39    Warning,
40    Info,
41    Hint,
42    Added,
43    Removed,
44    Unchanged,
45}
46
47impl Semantic {
48    /// The full variant set, in canonical order.
49    #[must_use]
50    pub const fn all() -> [Semantic; 16] {
51        use Semantic::{
52            Accent, Added, Comment, Error, Hint, Info, Keyword, KeywordArg, Literal, Muted, Number,
53            Removed, String, Symbol, Unchanged, Warning,
54        };
55        [
56            Keyword, Symbol, KeywordArg, String, Number, Literal, Comment, Accent, Muted, Error,
57            Warning, Info, Hint, Added, Removed, Unchanged,
58        ]
59    }
60}
61
62/// `Semantic -> HlClass` — total. Round-trips to identity through
63/// [`hlclass_to_semantic`] for every `Semantic` variant (see the test), now
64/// that `HlClass` carries the diagnostic + diff variants.
65impl From<Semantic> for HlClass {
66    fn from(s: Semantic) -> Self {
67        match s {
68            Semantic::Keyword => HlClass::Keyword,
69            Semantic::Symbol => HlClass::Punctuation,
70            Semantic::KeywordArg => HlClass::KeywordArg,
71            Semantic::String => HlClass::Str,
72            Semantic::Number => HlClass::Numeric { float: false },
73            Semantic::Literal => HlClass::Constant,
74            Semantic::Comment => HlClass::Comment { multiline: false },
75            Semantic::Accent => HlClass::Special,
76            Semantic::Muted => HlClass::Plain,
77            Semantic::Error => HlClass::Error,
78            Semantic::Warning => HlClass::Warning,
79            Semantic::Info => HlClass::Info,
80            Semantic::Hint => HlClass::Hint,
81            Semantic::Added => HlClass::Added,
82            Semantic::Removed => HlClass::Removed,
83            Semantic::Unchanged => HlClass::Unchanged,
84        }
85    }
86}
87
88/// `HlClass -> Semantic` — total. `HlClass` is richer (it carries `Type` /
89/// `Function` / `Escape` / … that `Semantic` lacks), so this direction is
90/// lossy for those: they fold to the nearest `Semantic` (documented per arm).
91/// The mapping is chosen so `Semantic -> HlClass -> Semantic` is the identity.
92#[must_use]
93pub fn hlclass_to_semantic(c: HlClass) -> Semantic {
94    match c {
95        HlClass::Keyword => Semantic::Keyword,
96        HlClass::KeywordArg => Semantic::KeywordArg,
97        HlClass::Str => Semantic::String,
98        HlClass::Numeric { .. } => Semantic::Number,
99        HlClass::Boolean | HlClass::Constant => Semantic::Literal,
100        HlClass::Comment { .. } => Semantic::Comment,
101        // accent-colored identifiers + emphasis fold to Accent.
102        HlClass::Type
103        | HlClass::Function
104        | HlClass::Namespace
105        | HlClass::Attribute
106        | HlClass::Escape
107        | HlClass::Special
108        | HlClass::Hyperlink => Semantic::Accent,
109        // symbolic tokens fold to Symbol.
110        HlClass::Punctuation | HlClass::Operator => Semantic::Symbol,
111        HlClass::Error => Semantic::Error,
112        HlClass::Warning => Semantic::Warning,
113        HlClass::Info => Semantic::Info,
114        HlClass::Hint => Semantic::Hint,
115        HlClass::Added => Semantic::Added,
116        HlClass::Removed => Semantic::Removed,
117        // normal / muted / whitespace text.
118        HlClass::Plain => Semantic::Muted,
119        HlClass::Variable | HlClass::Whitespace | HlClass::Unchanged => Semantic::Unchanged,
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::{HlClass, Semantic, hlclass_to_semantic};
126
127    #[test]
128    fn semantic_hlclass_roundtrips_to_identity() {
129        for s in Semantic::all() {
130            let hl: HlClass = s.into();
131            assert_eq!(
132                hlclass_to_semantic(hl),
133                s,
134                "Semantic -> HlClass -> Semantic must be identity for {s:?}",
135            );
136        }
137    }
138
139    #[test]
140    fn serde_is_snake_case_stable() {
141        assert_eq!(
142            serde_json::to_string(&Semantic::KeywordArg).unwrap(),
143            "\"keyword_arg\"",
144        );
145        let back: Semantic = serde_json::from_str("\"unchanged\"").unwrap();
146        assert_eq!(back, Semantic::Unchanged);
147    }
148}