use std::collections::BTreeMap;
use std::fs::File;
use std::ops::Deref;
use std::path::Path;
use serde::{Deserialize, Serialize};
use crate::error::ZaloResult;
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub enum Reference {
Self_,
Base,
Local(String),
OtherComplete(String),
OtherSpecific(String, String),
}
impl Reference {
pub fn is_local(&self) -> bool {
matches!(self, Reference::Local(_))
}
}
impl From<&str> for Reference {
fn from(value: &str) -> Self {
match value {
"$self" => Self::Self_,
"$base" => Self::Base,
s if s.starts_with('#') => Self::Local(s[1..].to_string()),
s if s.contains('#') => {
let (scope, rule) = s.split_once('#').unwrap();
Self::OtherSpecific(scope.to_string(), rule.to_string())
}
_ => Self::OtherComplete(value.to_string()),
}
}
}
fn deserialize_reference<'de, D>(deserializer: D) -> Result<Option<Reference>, D::Error>
where
D: serde::Deserializer<'de>,
{
let opt_string = Option::<String>::deserialize(deserializer)?;
Ok(opt_string.map(|s| Reference::from(s.as_str())))
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
enum BoolOrNumber {
Bool(bool),
Number(u8),
}
fn bool_or_number<'de, D>(deserializer: D) -> Result<bool, D::Error>
where
D: serde::Deserializer<'de>,
{
match BoolOrNumber::deserialize(deserializer)? {
BoolOrNumber::Bool(b) => Ok(b),
BoolOrNumber::Number(0) => Ok(false),
BoolOrNumber::Number(1) => Ok(true),
BoolOrNumber::Number(x) => Err(serde::de::Error::custom(format!(
"expected bool, 0, or 1, got {x}"
))),
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Captures(pub(crate) BTreeMap<usize, RawRule>);
impl Deref for Captures {
type Target = BTreeMap<usize, RawRule>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Deserialize)]
#[serde(untagged)]
enum CapturesFormat {
Object(BTreeMap<String, RawRule>),
Array(Vec<RawRule>),
}
impl<'de> Deserialize<'de> for Captures {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let mut out = BTreeMap::new();
match CapturesFormat::deserialize(deserializer) {
Ok(captures_format) => {
match captures_format {
CapturesFormat::Object(string_map) => {
for (key, value) in string_map {
if let Ok(idx) = key.parse::<usize>() {
out.insert(idx, value);
}
}
}
CapturesFormat::Array(array) => {
for (idx, value) in array.into_iter().enumerate() {
out.insert(idx, value);
}
}
}
Ok(Captures(out))
}
Err(_) => {
Ok(Captures(out))
}
}
}
}
#[derive(Deserialize)]
#[serde(untagged)]
enum RawRuleValue {
Vec(Vec<RawRule>),
Single(Box<RawRule>),
}
fn deserialize_repository_map<'de, D>(
deserializer: D,
) -> Result<BTreeMap<String, RawRule>, D::Error>
where
D: serde::Deserializer<'de>,
{
let raw_map = BTreeMap::<String, RawRuleValue>::deserialize(deserializer)?;
let mut result = BTreeMap::new();
let default = RawRule::default();
for (key, val) in raw_map {
let mut rule = match val {
RawRuleValue::Vec(rules) => RawRule {
patterns: rules,
..Default::default()
},
RawRuleValue::Single(rule) => *rule,
};
rule.patterns.retain(|x| x != &default);
result.insert(key, rule);
}
Ok(result)
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase", default)]
pub struct RawRule {
#[serde(deserialize_with = "deserialize_reference")]
pub include: Option<Reference>,
pub name: Option<String>,
pub content_name: Option<String>,
#[serde(rename = "match")]
pub match_: Option<String>,
pub captures: Captures,
pub begin: Option<String>,
pub begin_captures: Captures,
pub end: Option<String>,
pub end_captures: Captures,
#[serde(rename = "while")]
pub while_: Option<String>,
pub while_captures: Captures,
pub patterns: Vec<RawRule>,
#[serde(deserialize_with = "deserialize_repository_map")]
pub repository: BTreeMap<String, RawRule>,
#[serde(deserialize_with = "bool_or_number")]
pub apply_end_pattern_last: bool,
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all(deserialize = "camelCase"))]
pub struct RawGrammar {
pub name: String,
#[serde(default)]
pub display_name: Option<String>,
#[serde(default)]
pub file_types: Vec<String>,
pub scope_name: String,
#[serde(default, deserialize_with = "deserialize_repository_map")]
pub repository: BTreeMap<String, RawRule>,
#[serde(default)]
pub patterns: Vec<RawRule>,
#[serde(default)]
pub injections: BTreeMap<String, RawRule>,
#[serde(default)]
pub injection_selector: Option<String>,
#[serde(default)]
pub inject_to: Vec<String>,
}
impl RawGrammar {
pub fn load_from_file<P: AsRef<Path>>(path: P) -> ZaloResult<Self> {
let file = File::open(&path)?;
let raw_grammar = serde_json::from_reader(&file)?;
Ok(raw_grammar)
}
}
#[cfg(test)]
mod tests {
use std::fs;
use super::*;
#[test]
fn can_parse_all_grammars() {
let entries = fs::read_dir("grammars-themes/packages/tm-grammars/grammars")
.expect("Failed to read grammars directory");
for entry in entries {
let entry = entry.expect("Failed to read directory entry");
let path = entry.path();
assert!(RawGrammar::load_from_file(&path).is_ok());
}
}
#[test]
fn can_parse_references() {
let test_cases = vec![
("#value", Reference::Local("value".to_string())),
("#expressions", Reference::Local("expressions".to_string())),
("#comments", Reference::Local("comments".to_string())),
("#objectkey", Reference::Local("objectkey".to_string())),
(
"#stringcontent",
Reference::Local("stringcontent".to_string()),
),
("#blocks.tell", Reference::Local("blocks.tell".to_string())),
(
"#blocks.repeat",
Reference::Local("blocks.repeat".to_string()),
),
(
"#emmydoc.type",
Reference::Local("emmydoc.type".to_string()),
),
(
"#built-in.constant",
Reference::Local("built-in.constant".to_string()),
),
(
"#attributes.considering-ignoring",
Reference::Local("attributes.considering-ignoring".to_string()),
),
(
"#comments.nested",
Reference::Local("comments.nested".to_string()),
),
("$self", Reference::Self_),
("$base", Reference::Base),
(
"source.js",
Reference::OtherComplete("source.js".to_string()),
),
(
"source.java",
Reference::OtherComplete("source.java".to_string()),
),
(
"source.json",
Reference::OtherComplete("source.json".to_string()),
),
(
"text.html.basic",
Reference::OtherComplete("text.html.basic".to_string()),
),
(
"source.tsx",
Reference::OtherComplete("source.tsx".to_string()),
),
(
"source.css",
Reference::OtherComplete("source.css".to_string()),
),
(
"source.tsx#template-substitution-element",
Reference::OtherSpecific(
"source.tsx".to_string(),
"template-substitution-element".to_string(),
),
),
(
"source.ts#expression",
Reference::OtherSpecific("source.ts".to_string(), "expression".to_string()),
),
(
"text.html.basic#core-minus-invalid",
Reference::OtherSpecific(
"text.html.basic".to_string(),
"core-minus-invalid".to_string(),
),
),
(
"source.css#property-names",
Reference::OtherSpecific("source.css".to_string(), "property-names".to_string()),
),
(
"source.json#value",
Reference::OtherSpecific("source.json".to_string(), "value".to_string()),
),
("", Reference::OtherComplete("".to_string())),
("simple", Reference::OtherComplete("simple".to_string())),
];
for (input, expected) in test_cases {
assert_eq!(
Reference::from(input),
expected,
"Failed to parse reference: {}",
input
);
}
}
}