1use std::collections::HashMap;
86use std::fmt::Write as _;
87use std::path::{Path, PathBuf};
88
89use syntect::parsing::{ParseSyntaxError, SyntaxDefinition};
90use yaml_rust::yaml::Hash as YamlHash;
91use yaml_rust::{Yaml, YamlEmitter};
92
93use crate::raw::{RawCapture, RawGrammar, RawRule};
94
95#[derive(Debug, thiserror::Error)]
96pub enum Error {
97 #[error("invalid JSON: {0}")]
98 Json(#[from] serde_json::Error),
99 #[error("failed to read grammar file {path:?}: {source}")]
100 Io {
101 path: PathBuf,
102 #[source]
103 source: std::io::Error,
104 },
105 #[error("a `begin` rule needs a matching `end` pattern")]
106 BeginWithoutEnd,
107 #[error(
108 "`begin`/`while` rules (scope {scope:?}, while pattern {while_pattern:?}) aren't supported: \
109 `syntect` has no engine-level equivalent of TextMate's `while` at all, so there's nothing \
110 to translate this into (not just an unimplemented case in this crate)"
111 )]
112 WhileNotSupported {
113 scope: Option<String>,
114 while_pattern: String,
115 },
116 #[error(
117 "regex {pattern:?} uses a `\\G` anchor, which never matches through `syntect`'s `onig` \
118 backend (confirmed empirically: `syntect` runs each pattern as an independent search, \
119 not the iterative same-string session `\\G` needs to mean anything) — translating it \
120 would silently produce wrong parses (a context that never closes) rather than an error, \
121 so this is rejected instead"
122 )]
123 GAnchorNotSupported { pattern: String },
124 #[error("rule has none of `match`, `begin`/`end`, `include`, or nested `patterns`")]
125 EmptyRule,
126 #[error("non-numeric capture group index: {0:?}")]
127 BadCaptureIndex(String),
128 #[error("failed to emit intermediate sublime-syntax YAML: {0}")]
129 YamlEmit(#[from] std::fmt::Error),
130 #[error("failed to emit intermediate sublime-syntax YAML: {0}")]
131 YamlDump(#[from] yaml_rust::EmitError),
132 #[error("syntect rejected the generated sublime-syntax YAML: {0}")]
133 Syntax(#[from] ParseSyntaxError),
134}
135
136pub fn load(source: &str) -> Result<SyntaxDefinition, Error> {
139 let grammar = RawGrammar::from_json(source)?;
140 translate(&grammar)
141}
142
143pub fn load_file(path: impl AsRef<Path>) -> Result<SyntaxDefinition, Error> {
161 let path = path.as_ref();
162 let source = std::fs::read_to_string(path).map_err(|source| Error::Io {
163 path: path.to_path_buf(),
164 source,
165 })?;
166 load(&source)
167}
168
169pub fn translate(grammar: &RawGrammar) -> Result<SyntaxDefinition, Error> {
170 let yaml_text = to_sublime_syntax_yaml(grammar)?;
171 SyntaxDefinition::load_from_str(&yaml_text, false, grammar.name.as_deref())
172 .map_err(Error::Syntax)
173}
174
175pub fn to_sublime_syntax_yaml(grammar: &RawGrammar) -> Result<String, Error> {
180 let mut contexts = YamlHash::new();
181 let mut namer = Namer::default();
182
183 let main_items = translate_patterns(&grammar.patterns, &mut contexts, &mut namer)?;
184 contexts.insert(Yaml::String("main".to_string()), Yaml::Array(main_items));
185
186 let mut repo_names: Vec<&String> = grammar.repository.keys().collect();
187 repo_names.sort();
188 for name in repo_names {
189 let rule = &grammar.repository[name];
190 let items = repository_entry_to_context(rule, &mut contexts, &mut namer)?;
191 contexts.insert(Yaml::String(name.clone()), Yaml::Array(items));
192 }
193
194 let mut doc = YamlHash::new();
195 doc.insert(
196 Yaml::String("name".to_string()),
197 Yaml::String(
198 grammar
199 .name
200 .clone()
201 .unwrap_or_else(|| grammar.scope_name.clone()),
202 ),
203 );
204 doc.insert(
205 Yaml::String("scope".to_string()),
206 Yaml::String(grammar.scope_name.clone()),
207 );
208 if !grammar.file_types.is_empty() {
209 doc.insert(
210 Yaml::String("file_extensions".to_string()),
211 Yaml::Array(
212 grammar
213 .file_types
214 .iter()
215 .cloned()
216 .map(Yaml::String)
217 .collect(),
218 ),
219 );
220 }
221 if let Some(flm) = &grammar.first_line_match {
222 doc.insert(
223 Yaml::String("first_line_match".to_string()),
224 Yaml::String(flm.clone()),
225 );
226 }
227 doc.insert(Yaml::String("contexts".to_string()), Yaml::Hash(contexts));
228
229 let mut out = String::new();
230 writeln!(out, "%YAML 1.2")?;
233 YamlEmitter::new(&mut out).dump(&Yaml::Hash(doc))?;
234 Ok(out)
235}
236
237fn repository_entry_to_context(
238 rule: &RawRule,
239 contexts: &mut YamlHash,
240 namer: &mut Namer,
241) -> Result<Vec<Yaml>, Error> {
242 if rule.match_.is_none() && rule.begin.is_none() && rule.include.is_none() {
243 return translate_patterns(&rule.patterns, contexts, namer);
245 }
246 let item = rule_to_item(rule, contexts, namer)?;
247 Ok(vec![item])
248}
249
250fn translate_patterns(
251 rules: &[RawRule],
252 contexts: &mut YamlHash,
253 namer: &mut Namer,
254) -> Result<Vec<Yaml>, Error> {
255 rules
256 .iter()
257 .map(|r| rule_to_item(r, contexts, namer))
258 .collect()
259}
260
261fn rule_to_item(rule: &RawRule, contexts: &mut YamlHash, namer: &mut Namer) -> Result<Yaml, Error> {
262 if let Some(include) = &rule.include {
263 let mut item = YamlHash::new();
264 item.insert(
265 Yaml::String("include".to_string()),
266 Yaml::String(resolve_include(include)),
267 );
268 return Ok(Yaml::Hash(item));
269 }
270
271 if let Some(begin) = &rule.begin {
272 if let Some(while_pattern) = &rule.while_ {
273 return Err(Error::WhileNotSupported {
274 scope: rule.name.clone(),
275 while_pattern: while_pattern.clone(),
276 });
277 }
278 let end = rule.end.as_ref().ok_or(Error::BeginWithoutEnd)?;
279 reject_g_anchor(begin)?;
280 reject_g_anchor(end)?;
281
282 let body_name = namer.next("begin-end-body");
283 let mut body_items = Vec::new();
284 if let Some(name) = &rule.name {
285 let mut meta = YamlHash::new();
286 meta.insert(
287 Yaml::String("meta_scope".to_string()),
288 Yaml::String(name.clone()),
289 );
290 body_items.push(Yaml::Hash(meta));
291 }
292 if let Some(content_name) = &rule.content_name {
293 let mut meta = YamlHash::new();
294 meta.insert(
295 Yaml::String("meta_content_scope".to_string()),
296 Yaml::String(content_name.clone()),
297 );
298 body_items.push(Yaml::Hash(meta));
299 }
300
301 let mut end_item = YamlHash::new();
302 end_item.insert(Yaml::String("match".to_string()), Yaml::String(end.clone()));
303 if let Some(captures) = captures_yaml(&rule.end_captures)? {
304 end_item.insert(Yaml::String("captures".to_string()), captures);
305 }
306 end_item.insert(Yaml::String("pop".to_string()), Yaml::Boolean(true));
307 let end_item = Yaml::Hash(end_item);
308
309 let body_patterns = translate_patterns(&rule.patterns, contexts, namer)?;
310 if rule.apply_end_pattern_last {
313 body_items.extend(body_patterns);
314 body_items.push(end_item);
315 } else {
316 body_items.push(end_item);
317 body_items.extend(body_patterns);
318 }
319 contexts.insert(
320 Yaml::String(body_name.clone()),
321 Yaml::Array(body_items),
322 );
323
324 let mut begin_item = YamlHash::new();
325 begin_item.insert(
326 Yaml::String("match".to_string()),
327 Yaml::String(begin.clone()),
328 );
329 if let Some(captures) = captures_yaml(&rule.begin_captures)? {
330 begin_item.insert(Yaml::String("captures".to_string()), captures);
331 }
332 begin_item.insert(Yaml::String("push".to_string()), Yaml::String(body_name));
333 return Ok(Yaml::Hash(begin_item));
334 }
335
336 if let Some(m) = &rule.match_ {
337 reject_g_anchor(m)?;
338 let mut item = YamlHash::new();
339 item.insert(Yaml::String("match".to_string()), Yaml::String(m.clone()));
340 if let Some(name) = &rule.name {
341 item.insert(
342 Yaml::String("scope".to_string()),
343 Yaml::String(name.clone()),
344 );
345 }
346 if let Some(captures) = captures_yaml(&rule.captures)? {
347 item.insert(Yaml::String("captures".to_string()), captures);
348 }
349 return Ok(Yaml::Hash(item));
350 }
351
352 if !rule.patterns.is_empty() {
353 let name = namer.next("group");
355 let items = translate_patterns(&rule.patterns, contexts, namer)?;
356 contexts.insert(Yaml::String(name.clone()), Yaml::Array(items));
357 let mut item = YamlHash::new();
358 item.insert(Yaml::String("include".to_string()), Yaml::String(name));
359 return Ok(Yaml::Hash(item));
360 }
361
362 Err(Error::EmptyRule)
363}
364
365fn reject_g_anchor(pattern: &str) -> Result<(), Error> {
370 let mut backslash_run = 0usize;
371 for c in pattern.chars() {
372 if c == '\\' {
373 backslash_run += 1;
374 continue;
375 }
376 if c == 'G' && backslash_run % 2 == 1 {
377 return Err(Error::GAnchorNotSupported {
378 pattern: pattern.to_string(),
379 });
380 }
381 backslash_run = 0;
382 }
383 Ok(())
384}
385
386fn resolve_include(include: &str) -> String {
387 if include == "$self" || include == "$base" {
388 return "main".to_string();
389 }
390 if let Some(name) = include.strip_prefix('#') {
391 return name.to_string();
392 }
393 format!("scope:{include}")
394}
395
396fn captures_yaml(
397 captures: &Option<HashMap<String, RawCapture>>,
398) -> Result<Option<Yaml>, Error> {
399 let Some(map) = captures else {
400 return Ok(None);
401 };
402 let mut hash = YamlHash::new();
403 for (key, capture) in map {
404 let index: i64 = key
405 .parse()
406 .map_err(|_| Error::BadCaptureIndex(key.clone()))?;
407 let Some(name) = &capture.name else {
408 continue;
409 };
410 hash.insert(Yaml::Integer(index), Yaml::String(name.clone()));
411 }
412 if hash.is_empty() {
413 return Ok(None);
414 }
415 Ok(Some(Yaml::Hash(hash)))
416}
417
418#[derive(Default)]
419struct Namer {
420 counter: usize,
421}
422
423impl Namer {
424 fn next(&mut self, prefix: &str) -> String {
425 self.counter += 1;
426 format!("{prefix}-{}", self.counter)
427 }
428}