use std::collections::HashMap;
use std::fmt::Write as _;
use std::path::{Path, PathBuf};
use syntect::parsing::{ParseSyntaxError, SyntaxDefinition};
use yaml_rust::yaml::Hash as YamlHash;
use yaml_rust::{Yaml, YamlEmitter};
use crate::raw::{RawCapture, RawGrammar, RawRule};
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("invalid JSON: {0}")]
Json(#[from] serde_json::Error),
#[error("failed to read grammar file {path:?}: {source}")]
Io {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("a `begin` rule needs a matching `end` pattern")]
BeginWithoutEnd,
#[error(
"`begin`/`while` rules (scope {scope:?}, while pattern {while_pattern:?}) aren't supported: \
`syntect` has no engine-level equivalent of TextMate's `while` at all, so there's nothing \
to translate this into (not just an unimplemented case in this crate)"
)]
WhileNotSupported {
scope: Option<String>,
while_pattern: String,
},
#[error(
"regex {pattern:?} uses a `\\G` anchor, which never matches through `syntect`'s `onig` \
backend (confirmed empirically: `syntect` runs each pattern as an independent search, \
not the iterative same-string session `\\G` needs to mean anything) — translating it \
would silently produce wrong parses (a context that never closes) rather than an error, \
so this is rejected instead"
)]
GAnchorNotSupported { pattern: String },
#[error("rule has none of `match`, `begin`/`end`, `include`, or nested `patterns`")]
EmptyRule,
#[error("non-numeric capture group index: {0:?}")]
BadCaptureIndex(String),
#[error("failed to emit intermediate sublime-syntax YAML: {0}")]
YamlEmit(#[from] std::fmt::Error),
#[error("failed to emit intermediate sublime-syntax YAML: {0}")]
YamlDump(#[from] yaml_rust::EmitError),
#[error("syntect rejected the generated sublime-syntax YAML: {0}")]
Syntax(#[from] ParseSyntaxError),
}
pub fn load(source: &str) -> Result<SyntaxDefinition, Error> {
let grammar = RawGrammar::from_json(source)?;
translate(&grammar)
}
pub fn load_file(path: impl AsRef<Path>) -> Result<SyntaxDefinition, Error> {
let path = path.as_ref();
let source = std::fs::read_to_string(path).map_err(|source| Error::Io {
path: path.to_path_buf(),
source,
})?;
load(&source)
}
pub fn translate(grammar: &RawGrammar) -> Result<SyntaxDefinition, Error> {
let yaml_text = to_sublime_syntax_yaml(grammar)?;
SyntaxDefinition::load_from_str(&yaml_text, false, grammar.name.as_deref())
.map_err(Error::Syntax)
}
pub fn to_sublime_syntax_yaml(grammar: &RawGrammar) -> Result<String, Error> {
let mut contexts = YamlHash::new();
let mut namer = Namer::default();
let main_items = translate_patterns(&grammar.patterns, &mut contexts, &mut namer)?;
contexts.insert(Yaml::String("main".to_string()), Yaml::Array(main_items));
let mut repo_names: Vec<&String> = grammar.repository.keys().collect();
repo_names.sort();
for name in repo_names {
let rule = &grammar.repository[name];
let items = repository_entry_to_context(rule, &mut contexts, &mut namer)?;
contexts.insert(Yaml::String(name.clone()), Yaml::Array(items));
}
let mut doc = YamlHash::new();
doc.insert(
Yaml::String("name".to_string()),
Yaml::String(
grammar
.name
.clone()
.unwrap_or_else(|| grammar.scope_name.clone()),
),
);
doc.insert(
Yaml::String("scope".to_string()),
Yaml::String(grammar.scope_name.clone()),
);
if !grammar.file_types.is_empty() {
doc.insert(
Yaml::String("file_extensions".to_string()),
Yaml::Array(
grammar
.file_types
.iter()
.cloned()
.map(Yaml::String)
.collect(),
),
);
}
if let Some(flm) = &grammar.first_line_match {
doc.insert(
Yaml::String("first_line_match".to_string()),
Yaml::String(flm.clone()),
);
}
doc.insert(Yaml::String("contexts".to_string()), Yaml::Hash(contexts));
let mut out = String::new();
writeln!(out, "%YAML 1.2")?;
YamlEmitter::new(&mut out).dump(&Yaml::Hash(doc))?;
Ok(out)
}
fn repository_entry_to_context(
rule: &RawRule,
contexts: &mut YamlHash,
namer: &mut Namer,
) -> Result<Vec<Yaml>, Error> {
if rule.match_.is_none() && rule.begin.is_none() && rule.include.is_none() {
return translate_patterns(&rule.patterns, contexts, namer);
}
let item = rule_to_item(rule, contexts, namer)?;
Ok(vec![item])
}
fn translate_patterns(
rules: &[RawRule],
contexts: &mut YamlHash,
namer: &mut Namer,
) -> Result<Vec<Yaml>, Error> {
rules
.iter()
.map(|r| rule_to_item(r, contexts, namer))
.collect()
}
fn rule_to_item(rule: &RawRule, contexts: &mut YamlHash, namer: &mut Namer) -> Result<Yaml, Error> {
if let Some(include) = &rule.include {
let mut item = YamlHash::new();
item.insert(
Yaml::String("include".to_string()),
Yaml::String(resolve_include(include)),
);
return Ok(Yaml::Hash(item));
}
if let Some(begin) = &rule.begin {
if let Some(while_pattern) = &rule.while_ {
return Err(Error::WhileNotSupported {
scope: rule.name.clone(),
while_pattern: while_pattern.clone(),
});
}
let end = rule.end.as_ref().ok_or(Error::BeginWithoutEnd)?;
reject_g_anchor(begin)?;
reject_g_anchor(end)?;
let body_name = namer.next("begin-end-body");
let mut body_items = Vec::new();
if let Some(name) = &rule.name {
let mut meta = YamlHash::new();
meta.insert(
Yaml::String("meta_scope".to_string()),
Yaml::String(name.clone()),
);
body_items.push(Yaml::Hash(meta));
}
if let Some(content_name) = &rule.content_name {
let mut meta = YamlHash::new();
meta.insert(
Yaml::String("meta_content_scope".to_string()),
Yaml::String(content_name.clone()),
);
body_items.push(Yaml::Hash(meta));
}
let mut end_item = YamlHash::new();
end_item.insert(Yaml::String("match".to_string()), Yaml::String(end.clone()));
if let Some(captures) = captures_yaml(&rule.end_captures)? {
end_item.insert(Yaml::String("captures".to_string()), captures);
}
end_item.insert(Yaml::String("pop".to_string()), Yaml::Boolean(true));
let end_item = Yaml::Hash(end_item);
let body_patterns = translate_patterns(&rule.patterns, contexts, namer)?;
if rule.apply_end_pattern_last {
body_items.extend(body_patterns);
body_items.push(end_item);
} else {
body_items.push(end_item);
body_items.extend(body_patterns);
}
contexts.insert(
Yaml::String(body_name.clone()),
Yaml::Array(body_items),
);
let mut begin_item = YamlHash::new();
begin_item.insert(
Yaml::String("match".to_string()),
Yaml::String(begin.clone()),
);
if let Some(captures) = captures_yaml(&rule.begin_captures)? {
begin_item.insert(Yaml::String("captures".to_string()), captures);
}
begin_item.insert(Yaml::String("push".to_string()), Yaml::String(body_name));
return Ok(Yaml::Hash(begin_item));
}
if let Some(m) = &rule.match_ {
reject_g_anchor(m)?;
let mut item = YamlHash::new();
item.insert(Yaml::String("match".to_string()), Yaml::String(m.clone()));
if let Some(name) = &rule.name {
item.insert(
Yaml::String("scope".to_string()),
Yaml::String(name.clone()),
);
}
if let Some(captures) = captures_yaml(&rule.captures)? {
item.insert(Yaml::String("captures".to_string()), captures);
}
return Ok(Yaml::Hash(item));
}
if !rule.patterns.is_empty() {
let name = namer.next("group");
let items = translate_patterns(&rule.patterns, contexts, namer)?;
contexts.insert(Yaml::String(name.clone()), Yaml::Array(items));
let mut item = YamlHash::new();
item.insert(Yaml::String("include".to_string()), Yaml::String(name));
return Ok(Yaml::Hash(item));
}
Err(Error::EmptyRule)
}
fn reject_g_anchor(pattern: &str) -> Result<(), Error> {
let mut backslash_run = 0usize;
for c in pattern.chars() {
if c == '\\' {
backslash_run += 1;
continue;
}
if c == 'G' && backslash_run % 2 == 1 {
return Err(Error::GAnchorNotSupported {
pattern: pattern.to_string(),
});
}
backslash_run = 0;
}
Ok(())
}
fn resolve_include(include: &str) -> String {
if include == "$self" || include == "$base" {
return "main".to_string();
}
if let Some(name) = include.strip_prefix('#') {
return name.to_string();
}
format!("scope:{include}")
}
fn captures_yaml(
captures: &Option<HashMap<String, RawCapture>>,
) -> Result<Option<Yaml>, Error> {
let Some(map) = captures else {
return Ok(None);
};
let mut hash = YamlHash::new();
for (key, capture) in map {
let index: i64 = key
.parse()
.map_err(|_| Error::BadCaptureIndex(key.clone()))?;
let Some(name) = &capture.name else {
continue;
};
hash.insert(Yaml::Integer(index), Yaml::String(name.clone()));
}
if hash.is_empty() {
return Ok(None);
}
Ok(Some(Yaml::Hash(hash)))
}
#[derive(Default)]
struct Namer {
counter: usize,
}
impl Namer {
fn next(&mut self, prefix: &str) -> String {
self.counter += 1;
format!("{prefix}-{}", self.counter)
}
}