Skip to main content

rustik_highlight/
raw.rs

1//! Raw TextMate JSON data structures.
2//!
3//! These types mirror the loose JSON shapes accepted by TextMate and VS Code
4//! grammar/theme files before the crate compiles them into stricter runtime
5//! data. Keeping this layer separate lets parsing tolerate source-file quirks
6//! without leaking those shapes into tokenization and styling code.
7
8use std::collections::BTreeMap;
9use std::str::FromStr;
10
11use serde::{Deserialize, json as serde_json};
12
13use crate::Error;
14
15/// A parsed, uncompiled TextMate grammar.
16#[derive(Clone, Debug, Deserialize)]
17pub struct RawGrammar {
18    /// Human-readable display name.
19    #[serde(rename = "displayName")]
20    pub display_name: Option<String>,
21    /// Human-readable grammar name.
22    pub name: String,
23    /// Root scope name.
24    #[serde(rename = "scopeName")]
25    pub scope_name: String,
26    /// File extensions and special file names.
27    #[serde(rename = "fileTypes")]
28    pub file_types: Option<Vec<String>>,
29    /// First-line match expressions, as either a string or string array.
30    #[serde(rename = "firstLineMatch")]
31    pub first_line_match: Option<serde_json::Value>,
32    /// Root patterns.
33    pub patterns: Vec<RawPattern>,
34    /// Named pattern repository.
35    pub repository: Option<BTreeMap<String, RawPattern>>,
36}
37
38/// A parsed, uncompiled TextMate pattern.
39#[derive(Clone, Debug, Default, Deserialize)]
40pub struct RawPattern {
41    /// Scope assigned to the match.
42    pub name: Option<String>,
43    /// Single-line match expression.
44    #[serde(rename = "match")]
45    pub match_rule: Option<String>,
46    /// Begin expression for a stateful rule.
47    pub begin: Option<String>,
48    /// End expression for a stateful rule.
49    pub end: Option<String>,
50    /// Nested patterns.
51    pub patterns: Option<Vec<RawPattern>>,
52    /// Include target such as `$self`, `$base`, or `#name`.
53    pub include: Option<String>,
54    /// Match captures.
55    pub captures: Option<BTreeMap<String, RawCapture>>,
56    /// Begin captures.
57    #[serde(rename = "beginCaptures")]
58    pub begin_captures: Option<BTreeMap<String, RawCapture>>,
59    /// End captures.
60    #[serde(rename = "endCaptures")]
61    pub end_captures: Option<BTreeMap<String, RawCapture>>,
62}
63
64/// A parsed, uncompiled TextMate capture.
65#[derive(Clone, Debug, Deserialize)]
66pub struct RawCapture {
67    /// Scope assigned to the captured group.
68    pub name: Option<String>,
69}
70
71/// A parsed, uncompiled TextMate theme.
72#[derive(Clone, Debug, Deserialize)]
73pub struct RawTheme {
74    /// Theme name.
75    pub name: String,
76    /// TextMate theme settings.
77    pub settings: Option<Vec<RawThemeRule>>,
78    /// VS Code token color rules.
79    #[serde(rename = "tokenColors")]
80    pub token_colors: Option<Vec<RawThemeRule>>,
81}
82
83/// One parsed TextMate theme rule.
84#[derive(Clone, Debug, Deserialize)]
85pub struct RawThemeRule {
86    /// Optional selector string or selector array.
87    pub scope: Option<serde_json::Value>,
88    /// Style settings.
89    pub settings: RawStyle,
90}
91
92/// Parsed TextMate style settings.
93#[derive(Clone, Debug, Default, Deserialize)]
94pub struct RawStyle {
95    /// Foreground color as `#rrggbb`.
96    pub foreground: Option<String>,
97    /// Space-separated text style flags.
98    #[serde(rename = "fontStyle")]
99    pub font_style: Option<String>,
100}
101
102impl RawGrammar {
103    /// Parses a raw grammar from JSON.
104    pub fn parse(input: &str) -> Result<Self, Error> {
105        input.parse()
106    }
107}
108
109impl FromStr for RawGrammar {
110    type Err = Error;
111
112    fn from_str(input: &str) -> Result<Self, Self::Err> {
113        serde_json::from_str(input).map_err(|_| Error::InvalidGrammar)
114    }
115}
116
117impl RawTheme {
118    /// Parses a raw theme from JSON.
119    pub fn parse(input: &str) -> Result<Self, Error> {
120        input.parse()
121    }
122}
123
124impl FromStr for RawTheme {
125    type Err = Error;
126
127    fn from_str(input: &str) -> Result<Self, Self::Err> {
128        serde_json::from_str(input).map_err(|_| Error::InvalidTheme)
129    }
130}
131
132impl RawThemeRule {
133    /// Returns normalized selector strings from a TextMate `scope` value.
134    pub(crate) fn scope_selectors(&self) -> Vec<String> {
135        let Some(scope) = self.scope.as_ref() else {
136            return Vec::new();
137        };
138        let mut selectors = Vec::new();
139        match scope {
140            serde_json::Value::String(scope) => push_selectors(scope, &mut selectors),
141            serde_json::Value::Array(scopes) => {
142                for scope in scopes {
143                    if let serde_json::Value::String(scope) = scope {
144                        push_selectors(scope, &mut selectors);
145                    }
146                }
147            }
148            _ => {}
149        }
150        selectors
151    }
152}
153
154/// Normalizes `firstLineMatch` from TextMate's string-or-array shape.
155pub(crate) fn first_line_patterns(value: Option<&serde_json::Value>) -> Vec<String> {
156    match value {
157        Some(serde_json::Value::String(pattern)) => vec![pattern.clone()],
158        Some(serde_json::Value::Array(values)) => values
159            .iter()
160            .filter_map(|value| {
161                if let serde_json::Value::String(pattern) = value {
162                    Some(pattern.clone())
163                } else {
164                    None
165                }
166            })
167            .collect(),
168        _ => Vec::new(),
169    }
170}
171
172/// Splits a comma-separated TextMate selector list.
173fn push_selectors(input: &str, selectors: &mut Vec<String>) {
174    selectors.extend(
175        input
176            .split(',')
177            .map(str::trim)
178            .filter(|selector| !selector.is_empty())
179            .map(str::to_owned),
180    );
181}