use std::collections::BTreeMap;
use crate::token::TokenKind;
pub(crate) const STANDARD_DELIMITERS: &str = " \n\t.():!+,-<=>%&*/;?[]^{|}~\\";
#[derive(Debug, Clone)]
pub struct Grammar {
pub name: String,
pub section: String,
pub extensions: Vec<String>,
pub alternative_names: Vec<String>,
pub priority: i64,
pub hidden: bool,
pub keyword_lists: BTreeMap<String, Vec<String>>,
pub keyword_includes: Vec<KeywordInclude>,
pub contexts: Vec<Context>,
pub keywords: KeywordSettings,
pub item_styles: BTreeMap<String, TokenKind>,
}
#[derive(Debug, Clone)]
pub struct KeywordInclude {
pub target_list: String,
pub source_list: String,
pub source_language: String,
}
#[derive(Debug, Clone)]
pub struct KeywordSettings {
pub case_sensitive: bool,
pub weak_deliminators: String,
pub additional_deliminators: String,
}
impl Default for KeywordSettings {
fn default() -> Self {
KeywordSettings {
case_sensitive: true,
weak_deliminators: String::new(),
additional_deliminators: String::new(),
}
}
}
impl KeywordSettings {
pub fn is_delimiter(&self, c: char) -> bool {
if self.weak_deliminators.contains(c) {
return false;
}
if self.additional_deliminators.contains(c) {
return true;
}
STANDARD_DELIMITERS.contains(c)
}
}
#[derive(Debug, Clone)]
#[allow(clippy::struct_field_names)]
pub struct Context {
pub name: String,
pub attribute: String,
pub line_end_context: ContextSwitch,
pub line_begin_context: ContextSwitch,
pub line_empty_context: Option<ContextSwitch>,
pub fallthrough: bool,
pub fallthrough_context: ContextSwitch,
pub dynamic: bool,
pub rules: Vec<Rule>,
}
#[derive(Debug, Clone)]
pub struct Rule {
pub matcher: Matcher,
pub attribute: Option<String>,
pub context: ContextSwitch,
pub look_ahead: bool,
pub first_non_space: bool,
pub column: Option<usize>,
pub dynamic: bool,
pub children: Vec<Rule>,
}
#[derive(Debug, Clone)]
pub enum Matcher {
DetectChar(char),
Detect2Chars(char, char),
AnyChar(String),
StringDetect { text: String, insensitive: bool },
WordDetect { text: String, insensitive: bool },
RegExpr {
pattern: String,
insensitive: bool,
minimal: bool,
},
Keyword(String),
Int,
Float,
HlCOct,
HlCHex,
HlCStringChar,
HlCChar,
RangeDetect { start: char, end: char },
DetectSpaces,
DetectIdentifier,
LineContinue(char),
IncludeRules {
target: ContextTarget,
include_attribute: bool,
},
Unsupported,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ContextSwitch {
pub pops: usize,
pub push: Option<ContextTarget>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ContextTarget {
Local(String),
Foreign {
language: String,
context: Option<String>,
},
}
impl ContextSwitch {
pub fn parse(raw: &str) -> Self {
let mut rest = raw.trim();
if rest.is_empty() || rest == "#stay" {
return ContextSwitch::default();
}
let mut pops = 0usize;
while let Some(after) = rest.strip_prefix("#pop") {
pops = pops.saturating_add(1);
rest = after;
}
rest = rest.strip_prefix('!').unwrap_or(rest);
let push = if rest.is_empty() || rest == "#stay" {
None
} else {
Some(ContextTarget::parse(rest))
};
ContextSwitch { pops, push }
}
}
impl ContextTarget {
pub fn parse(raw: &str) -> Self {
if let Some(idx) = raw.find("##") {
let context = &raw[..idx];
let language = &raw[idx + 2..];
ContextTarget::Foreign {
language: language.to_string(),
context: if context.is_empty() {
None
} else {
Some(context.to_string())
},
}
} else {
ContextTarget::Local(raw.to_string())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_stay_and_empty() {
assert_eq!(ContextSwitch::parse("#stay"), ContextSwitch::default());
assert_eq!(ContextSwitch::parse(""), ContextSwitch::default());
}
#[test]
fn parses_pops() {
assert_eq!(
ContextSwitch::parse("#pop"),
ContextSwitch {
pops: 1,
push: None
}
);
assert_eq!(
ContextSwitch::parse("#pop#pop#pop"),
ContextSwitch {
pops: 3,
push: None
}
);
}
#[test]
fn parses_pop_then_push() {
assert_eq!(
ContextSwitch::parse("#pop#pop!Normal"),
ContextSwitch {
pops: 2,
push: Some(ContextTarget::Local("Normal".to_string()))
}
);
}
#[test]
fn parses_local_and_foreign() {
assert_eq!(
ContextSwitch::parse("Normal"),
ContextSwitch {
pops: 0,
push: Some(ContextTarget::Local("Normal".to_string()))
}
);
assert_eq!(
ContextTarget::parse("Comment##Bash"),
ContextTarget::Foreign {
language: "Bash".to_string(),
context: Some("Comment".to_string())
}
);
assert_eq!(
ContextTarget::parse("##CSS"),
ContextTarget::Foreign {
language: "CSS".to_string(),
context: None
}
);
}
#[test]
fn delimiter_adjustments_apply() {
let mut k = KeywordSettings::default();
assert!(k.is_delimiter(' '));
assert!(!k.is_delimiter('a'));
k.additional_deliminators = "$".to_string();
assert!(k.is_delimiter('$'));
k.weak_deliminators = ".".to_string();
assert!(!k.is_delimiter('.'));
}
}