Skip to main content

syntect/parsing/
yaml_load.rs

1use super::regex::{Regex, Region};
2use super::scope::*;
3use super::syntax_definition::*;
4use std::collections::HashMap;
5use std::error::Error;
6use std::ops::DerefMut;
7use std::path::Path;
8use yaml_rust2::yaml::Hash;
9use yaml_rust2::{Yaml, YamlLoader};
10
11#[derive(Debug, thiserror::Error)]
12#[non_exhaustive]
13pub enum ParseSyntaxError {
14    /// Invalid YAML file syntax, or at least something yaml_rust2 can't handle
15    #[error("Invalid YAML file syntax: {0}")]
16    InvalidYaml(#[source] Box<dyn std::error::Error + Send + Sync>),
17    /// The file must contain at least one YAML document
18    #[error("The file must contain at least one YAML document")]
19    EmptyFile,
20    /// Some keys are required for something to be a valid `.sublime-syntax`
21    #[error("Missing mandatory key in YAML file: {0}")]
22    MissingMandatoryKey(&'static str),
23    /// Invalid regex
24    #[error("Error while compiling regex '{0}': {1}")]
25    RegexCompileError(String, #[source] Box<dyn Error + Send + Sync + 'static>),
26    /// A scope that syntect's scope implementation can't handle
27    #[error("Invalid scope: {0}")]
28    InvalidScope(ParseScopeError),
29    /// A reference to another file that is invalid
30    #[error("Invalid file reference")]
31    BadFileRef,
32    /// Syntaxes must have a context named "main"
33    #[error("Context 'main' is missing")]
34    MainMissing,
35    /// Some part of the YAML file is the wrong type (e.g a string but should be a list)
36    /// Sorry this doesn't give you any way to narrow down where this is.
37    /// Maybe use Sublime Text to figure it out.
38    #[error("Type mismatch")]
39    TypeMismatch,
40}
41
42fn get_key<'a, R, F: FnOnce(&'a Yaml) -> Option<R>>(
43    map: &'a Hash,
44    key: &'static str,
45    f: F,
46) -> Result<R, ParseSyntaxError> {
47    map.get(&Yaml::String(key.to_owned()))
48        .ok_or(ParseSyntaxError::MissingMandatoryKey(key))
49        .and_then(|x| f(x).ok_or(ParseSyntaxError::TypeMismatch))
50}
51
52fn str_to_scopes(s: &str, repo: &mut ScopeRepository) -> Result<Vec<Scope>, ParseSyntaxError> {
53    s.split_whitespace()
54        .map(|scope| repo.build(scope).map_err(ParseSyntaxError::InvalidScope))
55        .collect()
56}
57
58pub(crate) struct ParserState<'a> {
59    pub(crate) scope_repo: &'a mut ScopeRepository,
60    pub(crate) variables: HashMap<String, String>,
61    pub(crate) variable_regex: Regex,
62    pub(crate) backref_regex: Regex,
63    pub(crate) lines_include_newline: bool,
64    pub(crate) version: u32,
65    /// When true, `parse_regex` skips the `try_compile_regex` validation
66    /// step. This is set for syntaxes that use `extends:`, because their
67    /// regexes may reference variables from the parent that aren't available
68    /// yet at load time. The regexes will be re-validated by
69    /// `re_resolve_all_regexes` after `resolve_extends` merges variables.
70    pub(crate) defer_regex_validation: bool,
71}
72
73// `__start` must not include prototypes from the actual syntax definition,
74// otherwise it's possible that a prototype makes us pop out of `__start`.
75static START_CONTEXT: &str = "
76__start:
77    - meta_include_prototype: false
78    - match: ''
79      push: __main
80__main:
81    - include: main
82";
83
84impl SyntaxDefinition {
85    /// In case you want to create your own SyntaxDefinition's in memory from strings.
86    ///
87    /// Generally you should use a [`SyntaxSet`].
88    ///
89    /// `fallback_name` is an optional name to use when the YAML doesn't provide a `name` key.
90    ///
91    /// [`SyntaxSet`]: ../struct.SyntaxSet.html
92    pub fn load_from_str(
93        s: &str,
94        lines_include_newline: bool,
95        fallback_name: Option<&str>,
96    ) -> Result<SyntaxDefinition, ParseSyntaxError> {
97        let docs = match YamlLoader::load_from_str(s) {
98            Ok(x) => x,
99            Err(e) => return Err(ParseSyntaxError::InvalidYaml(Box::new(e))),
100        };
101        if docs.is_empty() {
102            return Err(ParseSyntaxError::EmptyFile);
103        }
104        let doc = &docs[0];
105        let mut scope_repo = lock_global_scope_repo();
106        SyntaxDefinition::parse_top_level(
107            doc,
108            scope_repo.deref_mut(),
109            lines_include_newline,
110            fallback_name,
111        )
112    }
113
114    fn parse_top_level(
115        doc: &Yaml,
116        scope_repo: &mut ScopeRepository,
117        lines_include_newline: bool,
118        fallback_name: Option<&str>,
119    ) -> Result<SyntaxDefinition, ParseSyntaxError> {
120        let h = doc.as_hash().ok_or(ParseSyntaxError::TypeMismatch)?;
121
122        let mut variables = HashMap::new();
123        if let Ok(map) = get_key(h, "variables", |x| x.as_hash()) {
124            for (key, value) in map.iter() {
125                if let (Some(key_str), Some(val_str)) = (key.as_str(), value.as_str()) {
126                    variables.insert(key_str.to_owned(), val_str.to_owned());
127                }
128            }
129        }
130        let has_extends = get_key(h, "extends", Some).is_ok();
131        let empty_contexts = Hash::new();
132        let contexts_hash = match get_key(h, "contexts", |x| x.as_hash()) {
133            Ok(hash) => hash,
134            // extends-only syntaxes (e.g. "Batch File (Compound)") inherit
135            // all contexts from their parent and have no `contexts:` key.
136            Err(_) if has_extends => &empty_contexts,
137            Err(e) => return Err(e),
138        };
139        let top_level_scope = scope_repo
140            .build(get_key(h, "scope", |x| x.as_str())?)
141            .map_err(ParseSyntaxError::InvalidScope)?;
142        let version = get_key(h, "version", |x| x.as_i64()).unwrap_or(1) as u32;
143
144        let mut state = ParserState {
145            scope_repo,
146            variables,
147            variable_regex: Regex::new(r"\{\{([A-Za-z0-9_]+)\}\}".into()),
148            backref_regex: Regex::new(r"\\\d".into()),
149            lines_include_newline,
150            version,
151            defer_regex_validation: has_extends,
152        };
153
154        let mut contexts = SyntaxDefinition::parse_contexts(contexts_hash, &mut state)?;
155        if !contexts.contains_key("main") && !has_extends {
156            return Err(ParseSyntaxError::MainMissing);
157        }
158
159        if contexts.contains_key("main") {
160            SyntaxDefinition::add_initial_contexts(&mut contexts, &mut state, top_level_scope);
161        }
162
163        let mut file_extensions = Vec::new();
164        for extension_key in &["file_extensions", "hidden_file_extensions"] {
165            if let Ok(v) = get_key(h, extension_key, |x| x.as_vec()) {
166                file_extensions.extend(v.iter().filter_map(|y| y.as_str().map(|s| s.to_owned())))
167            }
168        }
169
170        let extends = get_key(h, "extends", Some)
171            .ok()
172            .map(|y| {
173                if let Some(s) = y.as_str() {
174                    vec![s.to_owned()]
175                } else if let Some(seq) = y.as_vec() {
176                    seq.iter()
177                        .filter_map(|v| v.as_str().map(|s| s.to_owned()))
178                        .collect()
179                } else {
180                    vec![]
181                }
182            })
183            .unwrap_or_default();
184
185        let defn = SyntaxDefinition {
186            name: get_key(h, "name", |x| x.as_str())
187                .unwrap_or_else(|_| fallback_name.unwrap_or("Unnamed"))
188                .to_owned(),
189            scope: top_level_scope,
190            file_extensions,
191            // TODO maybe cache a compiled version of this Regex
192            first_line_match: get_key(h, "first_line_match", |x| x.as_str())
193                .ok()
194                .map(|s| Self::resolve_variables(s, &state)),
195            hidden: get_key(h, "hidden", |x| x.as_bool()).unwrap_or(false),
196
197            variables: state.variables,
198            contexts,
199            extends,
200            version,
201        };
202        Ok(defn)
203    }
204
205    fn parse_contexts(
206        map: &Hash,
207        state: &mut ParserState<'_>,
208    ) -> Result<HashMap<String, Context>, ParseSyntaxError> {
209        let mut contexts = HashMap::new();
210        for (key, value) in map.iter() {
211            if let (Some(name), Some(val_vec)) = (key.as_str(), value.as_vec()) {
212                let mut namer = ContextNamer::new(name);
213                SyntaxDefinition::parse_context(val_vec, state, &mut contexts, &mut namer)?;
214            }
215        }
216
217        Ok(contexts)
218    }
219
220    fn parse_context(
221        vec: &[Yaml],
222        // TODO: Maybe just pass the scope repo if that's all that's needed?
223        state: &mut ParserState<'_>,
224        contexts: &mut HashMap<String, Context>,
225        namer: &mut ContextNamer,
226    ) -> Result<String, ParseSyntaxError> {
227        // Every parsed context starts with `meta_include_prototype = None`
228        // (unset). YAML-explicit `meta_include_prototype: <bool>` later
229        // upgrades it to `Some(<bool>)`. The prototype context's own
230        // self-attachment is suppressed by the `no_prototype` set in
231        // `SyntaxSetBuilder::link_syntaxes`, so we don't need a distinct
232        // initial value for prototype vs. non-prototype contexts.
233        let mut context = Context::new(None);
234        let name = namer.next();
235
236        for y in vec.iter() {
237            let map = y.as_hash().ok_or(ParseSyntaxError::TypeMismatch)?;
238
239            let mut is_special = false;
240            if let Ok(x) = get_key(map, "meta_scope", |x| x.as_str()) {
241                context.meta_scope = str_to_scopes(x, state.scope_repo)?;
242                is_special = true;
243            }
244            if let Ok(x) = get_key(map, "meta_content_scope", |x| x.as_str()) {
245                context.meta_content_scope = str_to_scopes(x, state.scope_repo)?;
246                is_special = true;
247            }
248            if let Ok(x) = get_key(map, "meta_include_prototype", |x| x.as_bool()) {
249                context.meta_include_prototype = Some(x);
250                is_special = true;
251            }
252            if let Ok(true) = get_key(map, "meta_prepend", |x| x.as_bool()) {
253                context.merge_mode = ContextMergeMode::Prepend;
254                is_special = true;
255            }
256            if let Ok(true) = get_key(map, "meta_append", |x| x.as_bool()) {
257                context.merge_mode = ContextMergeMode::Append;
258                is_special = true;
259            }
260            if let Ok(true) = get_key(map, "clear_scopes", |x| x.as_bool()) {
261                context.clear_scopes = Some(ClearAmount::All);
262                is_special = true;
263            }
264            if let Ok(x) = get_key(map, "clear_scopes", |x| x.as_i64()) {
265                context.clear_scopes = Some(ClearAmount::TopN(x as usize));
266                is_special = true;
267            }
268            if !is_special {
269                if let Ok(x) = get_key(map, "include", Some) {
270                    let reference =
271                        SyntaxDefinition::parse_reference(x, state, contexts, namer, false)?;
272                    let apply_prototype =
273                        get_key(map, "apply_prototype", |x| x.as_bool()).unwrap_or(false);
274                    if apply_prototype {
275                        context
276                            .patterns
277                            .push(Pattern::IncludeWithPrototype(reference));
278                    } else {
279                        context.patterns.push(Pattern::Include(reference));
280                    }
281                } else {
282                    let pattern =
283                        SyntaxDefinition::parse_match_pattern(map, state, contexts, namer)?;
284                    if pattern.has_captures {
285                        context.uses_backrefs = true;
286                    }
287                    context.patterns.push(Pattern::Match(pattern));
288                }
289            }
290        }
291
292        contexts.insert(name.clone(), context);
293        Ok(name)
294    }
295
296    fn parse_reference(
297        y: &Yaml,
298        state: &mut ParserState<'_>,
299        contexts: &mut HashMap<String, Context>,
300        namer: &mut ContextNamer,
301        with_escape: bool,
302    ) -> Result<ContextReference, ParseSyntaxError> {
303        if let Some(s) = y.as_str() {
304            let parts: Vec<&str> = s.split('#').collect();
305            let sub_context = if parts.len() > 1 {
306                Some(parts[1].to_owned())
307            } else {
308                None
309            };
310            if parts[0].starts_with("scope:") {
311                Ok(ContextReference::ByScope {
312                    scope: state
313                        .scope_repo
314                        .build(&parts[0][6..])
315                        .map_err(ParseSyntaxError::InvalidScope)?,
316                    sub_context,
317                    with_escape,
318                })
319            } else if parts[0].ends_with(".sublime-syntax") {
320                let stem = Path::new(parts[0])
321                    .file_stem()
322                    .and_then(|x| x.to_str())
323                    .ok_or(ParseSyntaxError::BadFileRef)?;
324                Ok(ContextReference::File {
325                    name: stem.to_owned(),
326                    sub_context,
327                    with_escape,
328                })
329            } else {
330                Ok(ContextReference::Named(parts[0].to_owned()))
331            }
332        } else if let Some(v) = y.as_vec() {
333            let subname = SyntaxDefinition::parse_context(v, state, contexts, namer)?;
334            Ok(ContextReference::Inline(subname))
335        } else {
336            Err(ParseSyntaxError::TypeMismatch)
337        }
338    }
339
340    fn parse_match_pattern(
341        map: &Hash,
342        state: &mut ParserState<'_>,
343        contexts: &mut HashMap<String, Context>,
344        namer: &mut ContextNamer,
345    ) -> Result<MatchPattern, ParseSyntaxError> {
346        let raw_regex = get_key(map, "match", |x| x.as_str())?;
347        let raw_regex_owned = raw_regex.to_owned();
348        let regex_str = Self::parse_regex(raw_regex, state)?;
349
350        let scope = get_key(map, "scope", |x| x.as_str())
351            .ok()
352            .map(|s| str_to_scopes(s, state.scope_repo))
353            .unwrap_or_else(|| Ok(vec![]))?;
354
355        let captures = if let Ok(map) = get_key(map, "captures", |x| x.as_hash()) {
356            Some(Self::parse_captures(map, &regex_str, state)?)
357        } else {
358            None
359        };
360
361        let mut has_captures = false;
362        let operation = if let Ok(y) = get_key(map, "pop", |y| {
363            y.as_i64().or(match y.as_bool() {
364                Some(true) => Some(1),
365                _ => None,
366            })
367        }) {
368            // Thanks @wbond for letting me know this is the correct way to check for captures
369            has_captures = state
370                .backref_regex
371                .search(&regex_str, 0, regex_str.len(), None, true);
372            // In Sublime Text, `pop: N` + `embed:` pops N contexts then pushes
373            // the embedded syntax with escape priority.
374            if get_key(map, "embed", Some).is_ok() {
375                Self::parse_embed_op(map, state, contexts, namer, y as usize)?
376            } else if let Ok(b) = get_key(map, "branch", |x| x.as_vec()) {
377                let branch_point = get_key(map, "branch_point", |x| x.as_str())?;
378                let alternatives: Vec<ContextReference> = b
379                    .iter()
380                    .map(|item| {
381                        SyntaxDefinition::parse_reference(item, state, contexts, namer, false)
382                    })
383                    .collect::<Result<_, _>>()?;
384                MatchOperation::Branch {
385                    name: branch_point.to_owned(),
386                    alternatives,
387                    pop_count: y as usize,
388                }
389            } else if let Ok(s) = get_key(map, "set", Some) {
390                // `pop: N + set: X` pops N contexts then pushes X.
391                MatchOperation::Set {
392                    ctx_refs: SyntaxDefinition::parse_pushargs(s, state, contexts, namer)?,
393                    pop_count: y as usize,
394                }
395            } else {
396                MatchOperation::Pop(y as usize)
397            }
398        } else if let Ok(y) = get_key(map, "push", Some) {
399            MatchOperation::Push(SyntaxDefinition::parse_pushargs(y, state, contexts, namer)?)
400        } else if let Ok(y) = get_key(map, "set", Some) {
401            MatchOperation::Set {
402                ctx_refs: SyntaxDefinition::parse_pushargs(y, state, contexts, namer)?,
403                pop_count: 1,
404            }
405        } else if let Ok(y) = get_key(map, "branch", |x| x.as_vec()) {
406            let branch_point = get_key(map, "branch_point", |x| x.as_str())?;
407            let alternatives: Vec<ContextReference> = y
408                .iter()
409                .map(|item| SyntaxDefinition::parse_reference(item, state, contexts, namer, false))
410                .collect::<Result<_, _>>()?;
411            MatchOperation::Branch {
412                name: branch_point.to_owned(),
413                alternatives,
414                pop_count: 0,
415            }
416        } else if let Ok(y) = get_key(map, "fail", |x| x.as_str()) {
417            MatchOperation::Fail(y.to_owned())
418        } else if get_key(map, "embed", Some).is_ok() {
419            Self::parse_embed_op(map, state, contexts, namer, 0)?
420        } else {
421            MatchOperation::None
422        };
423
424        let with_prototype = if let Ok(v) = get_key(map, "with_prototype", |x| x.as_vec()) {
425            // should a with_prototype include the prototype? I don't think so.
426            let subname = Self::parse_context(v, state, contexts, namer)?;
427            Some(ContextReference::Inline(subname))
428        } else {
429            None
430        };
431
432        let pattern = MatchPattern::new_with_raw(
433            has_captures,
434            regex_str,
435            raw_regex_owned,
436            scope,
437            captures,
438            operation,
439            with_prototype,
440        );
441
442        Ok(pattern)
443    }
444
445    fn parse_embed_op(
446        map: &Hash,
447        state: &mut ParserState<'_>,
448        contexts: &mut HashMap<String, Context>,
449        namer: &mut ContextNamer,
450        pop_count: usize,
451    ) -> Result<MatchOperation, ParseSyntaxError> {
452        let y = get_key(map, "embed", Some)?;
453        let v = get_key(map, "escape", Some)
454            .map_err(|_| ParseSyntaxError::MissingMandatoryKey("escape"))?;
455
456        let escape_raw = v.as_str().ok_or(ParseSyntaxError::TypeMismatch)?;
457        let escape_regex_str = Self::parse_regex(escape_raw, state)?;
458        let escape_has_captures =
459            state
460                .backref_regex
461                .search(&escape_regex_str, 0, escape_regex_str.len(), None, true);
462
463        let escape_captures = if let Ok(cap_map) = get_key(map, "escape_captures", |x| x.as_hash())
464        {
465            Some(Self::parse_captures(cap_map, &escape_regex_str, state)?)
466        } else {
467            None
468        };
469
470        let escape_info = EscapeInfo {
471            escape_regex: Regex::new(escape_regex_str),
472            has_captures: escape_has_captures,
473            escape_captures,
474            raw_escape_regex_str: Some(escape_raw.to_owned()),
475        };
476
477        // Build the wrapper context for embed_scope (meta_content_scope)
478        // and the embedded context reference
479        let mut embed_contexts = Vec::new();
480
481        // Create wrapper context with embed_scope if present
482        let has_embed_scope = get_key(map, "embed_scope", Some).is_ok();
483        if has_embed_scope {
484            let mut embed_scope_context_yaml = vec![];
485            let mut commands = Hash::new();
486            commands.insert(
487                Yaml::String("meta_include_prototype".to_string()),
488                Yaml::Boolean(false),
489            );
490            embed_scope_context_yaml.push(Yaml::Hash(commands));
491            if let Ok(s) = get_key(map, "embed_scope", Some) {
492                let mut commands2 = Hash::new();
493                commands2.insert(Yaml::String("meta_content_scope".to_string()), s.clone());
494                embed_scope_context_yaml.push(Yaml::Hash(commands2));
495            }
496            // Add a match-all to pass through to next context
497            let mut match_map = Hash::new();
498            match_map.insert(
499                Yaml::String("match".to_string()),
500                Yaml::String(String::new()),
501            );
502            match_map.insert(Yaml::String("pop".to_string()), Yaml::Boolean(true));
503            embed_scope_context_yaml.push(Yaml::Hash(match_map));
504            let scope_ctx_name =
505                SyntaxDefinition::parse_context(&embed_scope_context_yaml, state, contexts, namer)?;
506            // In v2, embed_scope replaces the embedded syntax's scope
507            if state.version >= 2 {
508                if let Some(ctx) = contexts.get_mut(&scope_ctx_name) {
509                    ctx.embed_scope_replaces = true;
510                }
511            }
512            embed_contexts.push(ContextReference::Inline(scope_ctx_name));
513        }
514
515        embed_contexts.push(SyntaxDefinition::parse_reference(
516            y, state, contexts, namer, true,
517        )?);
518
519        Ok(MatchOperation::Embed {
520            contexts: embed_contexts,
521            escape: escape_info,
522            pop_count,
523        })
524    }
525
526    fn parse_pushargs(
527        y: &Yaml,
528        state: &mut ParserState<'_>,
529        contexts: &mut HashMap<String, Context>,
530        namer: &mut ContextNamer,
531    ) -> Result<Vec<ContextReference>, ParseSyntaxError> {
532        // check for a push of multiple items
533        if y.as_vec().is_some_and(|v| {
534            !v.is_empty()
535                && (v[0].as_str().is_some()
536                    || (v[0].as_vec().is_some() && v[0].as_vec().unwrap()[0].as_hash().is_some()))
537        }) {
538            // this works because Result implements FromIterator to handle the errors
539            y.as_vec()
540                .unwrap()
541                .iter()
542                .map(|x| SyntaxDefinition::parse_reference(x, state, contexts, namer, false))
543                .collect()
544        } else {
545            let reference = SyntaxDefinition::parse_reference(y, state, contexts, namer, false)?;
546            Ok(vec![reference])
547        }
548    }
549
550    fn parse_regex(raw_regex: &str, state: &ParserState<'_>) -> Result<String, ParseSyntaxError> {
551        let regex = Self::resolve_variables(raw_regex, state);
552        let regex = replace_posix_char_classes(regex);
553        let regex = if state.lines_include_newline {
554            regex_for_newlines(regex)
555        } else {
556            // If the passed in strings don't include newlines (unlike Sublime) we can't match on
557            // them using the original regex. So this tries to rewrite the regex in a way that
558            // allows matching against lines without newlines (essentially replacing `\n` with `$`).
559            regex_for_no_newlines(regex)
560        };
561        if !state.defer_regex_validation {
562            Self::try_compile_regex(&regex)?;
563        }
564        Ok(regex)
565    }
566
567    fn resolve_variables(raw_regex: &str, state: &ParserState<'_>) -> String {
568        let mut result = String::new();
569        let mut index = 0;
570        let mut region = Region::new();
571        while state.variable_regex.search(
572            raw_regex,
573            index,
574            raw_regex.len(),
575            Some(&mut region),
576            true,
577        ) {
578            let (begin, end) = region.pos(0).unwrap();
579
580            result.push_str(&raw_regex[index..begin]);
581
582            let var_pos = region.pos(1).unwrap();
583            let var_name = &raw_regex[var_pos.0..var_pos.1];
584            let var_raw = state
585                .variables
586                .get(var_name)
587                .map(String::as_ref)
588                .unwrap_or("");
589            let var_resolved = Self::resolve_variables(var_raw, state);
590            result.push_str(&var_resolved);
591
592            index = end;
593        }
594        if index < raw_regex.len() {
595            result.push_str(&raw_regex[index..]);
596        }
597        result
598    }
599
600    fn try_compile_regex(regex_str: &str) -> Result<(), ParseSyntaxError> {
601        // Replace backreferences with a placeholder value that will also appear in errors
602        let regex_str =
603            substitute_backrefs_in_regex(regex_str, |i| Some(format!("<placeholder_{}>", i)));
604
605        if let Some(error) = Regex::try_compile(&regex_str) {
606            Err(ParseSyntaxError::RegexCompileError(regex_str, error))
607        } else {
608            Ok(())
609        }
610    }
611
612    fn parse_captures(
613        map: &Hash,
614        regex_str: &str,
615        state: &mut ParserState<'_>,
616    ) -> Result<CaptureMapping, ParseSyntaxError> {
617        let valid_indexes = get_consuming_capture_indexes(regex_str);
618        let mut captures = Vec::new();
619        for (key, value) in map.iter() {
620            if let (Some(key_int), Some(val_str)) = (key.as_i64(), value.as_str()) {
621                if valid_indexes.contains(&(key_int as usize)) {
622                    captures.push((key_int as usize, str_to_scopes(val_str, state.scope_repo)?));
623                }
624            }
625        }
626        Ok(captures)
627    }
628
629    /// Sublime treats the top level context slightly differently from
630    /// including the main context from other syntaxes. When main is popped
631    /// it is immediately re-added and when it is `set` over the file level
632    /// scope remains. This behaviour is emulated through some added contexts
633    /// that are the actual top level contexts used in parsing.
634    /// See <https://github.com/trishume/syntect/issues/58> for more.
635    pub(crate) fn add_initial_contexts(
636        contexts: &mut HashMap<String, Context>,
637        state: &mut ParserState<'_>,
638        top_level_scope: Scope,
639    ) {
640        let yaml_docs = YamlLoader::load_from_str(START_CONTEXT).unwrap();
641        let yaml = &yaml_docs[0];
642
643        let start_yaml: &[Yaml] = yaml["__start"].as_vec().unwrap();
644        SyntaxDefinition::parse_context(
645            start_yaml,
646            state,
647            contexts,
648            &mut ContextNamer::new("__start"),
649        )
650        .unwrap();
651        if let Some(start) = contexts.get_mut("__start") {
652            start.meta_content_scope = vec![top_level_scope];
653        }
654
655        let main_yaml: &[Yaml] = yaml["__main"].as_vec().unwrap();
656        SyntaxDefinition::parse_context(
657            main_yaml,
658            state,
659            contexts,
660            &mut ContextNamer::new("__main"),
661        )
662        .unwrap();
663
664        let meta_include_prototype = contexts["main"].meta_include_prototype;
665        let meta_scope = contexts["main"].meta_scope.clone();
666        // Copy `main`'s meta_content_scope to `__main`, but strip the
667        // auto-inserted `top_level_scope` at position 0 if present.
668        // On a fresh load `main.meta_content_scope` is still pre-insert
669        // here, so this is a no-op. On a re-run (from `resolve_extends`
670        // after a child inherits its parent's contexts), `main` already
671        // carries `top_level_scope` from the first call; copying it to
672        // `__main` would make both push the file scope at runtime,
673        // producing duplicates like `[source.diff.git, source.diff.git]`
674        // for Git Diff.
675        let mut meta_content_scope = contexts["main"].meta_content_scope.clone();
676        if meta_content_scope.first() == Some(&top_level_scope) {
677            meta_content_scope.remove(0);
678        }
679
680        if let Some(outer_main) = contexts.get_mut("__main") {
681            outer_main.meta_include_prototype = meta_include_prototype;
682            outer_main.meta_scope = meta_scope;
683            outer_main.meta_content_scope = meta_content_scope;
684        }
685
686        // add the top_level_scope as a meta_content_scope to main so
687        // pushes from other syntaxes add the file scope.
688        // Idempotent so a re-run (from `resolve_extends`) doesn't
689        // double-insert — see the comment on the copy above.
690        // TODO: this order is not quite correct if main also has a meta_scope
691        if let Some(main) = contexts.get_mut("main") {
692            if main.meta_content_scope.first() != Some(&top_level_scope) {
693                main.meta_content_scope.insert(0, top_level_scope);
694            }
695        }
696    }
697}
698
699/// Re-resolve a raw regex string with the given variables and newline mode.
700/// Applies the full pipeline: resolve_variables → replace_posix → newlines → try_compile.
701pub(crate) fn re_resolve_regex(
702    raw: &str,
703    variables: &HashMap<String, String>,
704    lines_include_newline: bool,
705) -> Result<String, ParseSyntaxError> {
706    let variable_regex = Regex::new(r"\{\{([A-Za-z0-9_]+)\}\}".into());
707    let state = ReResolveState {
708        variables,
709        variable_regex: &variable_regex,
710    };
711    let regex = re_resolve_variables(raw, &state);
712    let regex = replace_posix_char_classes(regex);
713    let regex = if lines_include_newline {
714        regex_for_newlines(regex)
715    } else {
716        regex_for_no_newlines(regex)
717    };
718    SyntaxDefinition::try_compile_regex(&regex)?;
719    Ok(regex)
720}
721
722struct ReResolveState<'a> {
723    variables: &'a HashMap<String, String>,
724    variable_regex: &'a Regex,
725}
726
727fn re_resolve_variables(raw_regex: &str, state: &ReResolveState<'_>) -> String {
728    let mut result = String::new();
729    let mut index = 0;
730    let mut region = Region::new();
731    while state
732        .variable_regex
733        .search(raw_regex, index, raw_regex.len(), Some(&mut region), true)
734    {
735        let (begin, end) = region.pos(0).unwrap();
736        result.push_str(&raw_regex[index..begin]);
737
738        let var_pos = region.pos(1).unwrap();
739        let var_name = &raw_regex[var_pos.0..var_pos.1];
740        let var_raw = state
741            .variables
742            .get(var_name)
743            .map(String::as_ref)
744            .unwrap_or("");
745        let var_resolved = re_resolve_variables(var_raw, state);
746        result.push_str(&var_resolved);
747
748        index = end;
749    }
750    if index < raw_regex.len() {
751        result.push_str(&raw_regex[index..]);
752    }
753    result
754}
755
756/// Re-resolve all regexes in a SyntaxDefinition that have a stored raw_regex_str.
757/// This is used after merging variables during extends resolution.
758pub(crate) fn re_resolve_all_regexes(
759    syntax: &mut SyntaxDefinition,
760    lines_include_newline: bool,
761) -> Result<(), ParseSyntaxError> {
762    for context in syntax.contexts.values_mut() {
763        for pattern in &mut context.patterns {
764            if let Pattern::Match(ref mut match_pat) = pattern {
765                if let Some(ref raw) = match_pat.raw_regex_str {
766                    let new_regex_str =
767                        re_resolve_regex(raw, &syntax.variables, lines_include_newline)?;
768                    match_pat.regex = Regex::new(new_regex_str);
769                }
770                // Also re-resolve the escape regex for embed operations
771                if let MatchOperation::Embed { ref mut escape, .. } = match_pat.operation {
772                    if let Some(ref raw) = escape.raw_escape_regex_str {
773                        let new_regex_str =
774                            re_resolve_regex(raw, &syntax.variables, lines_include_newline)?;
775                        escape.escape_regex = Regex::new(new_regex_str);
776                    }
777                }
778            }
779        }
780    }
781    Ok(())
782}
783
784struct ContextNamer {
785    name: String,
786    anonymous_index: Option<usize>,
787}
788
789impl ContextNamer {
790    fn new(name: &str) -> ContextNamer {
791        ContextNamer {
792            name: name.to_string(),
793            anonymous_index: None,
794        }
795    }
796
797    fn next(&mut self) -> String {
798        let name = if let Some(index) = self.anonymous_index {
799            format!("#anon_{}_{}", self.name, index)
800        } else {
801            self.name.clone()
802        };
803
804        self.anonymous_index = Some(self.anonymous_index.map(|i| i + 1).unwrap_or(0));
805        name
806    }
807}
808
809/// In fancy-regex, POSIX character classes only match ASCII characters.
810///
811/// Sublime's syntaxes expect them to match Unicode characters as well, so transform them to
812/// corresponding Unicode character classes.
813fn replace_posix_char_classes(regex: String) -> String {
814    regex
815        .replace("[:alpha:]", r"\p{L}")
816        .replace("[:alnum:]", r"\p{L}\p{N}")
817        .replace("[:lower:]", r"\p{Ll}")
818        .replace("[:upper:]", r"\p{Lu}")
819        .replace("[:digit:]", r"\p{Nd}")
820}
821
822/// Some of the regexes include `$` and expect it to match end of line,
823/// e.g. *before* the `\n` in `test\n`.
824///
825/// In fancy-regex, `$` means end of text by default, so that would
826/// match *after* `\n`. Using `(?m:$)` instead means it matches end of line.
827///
828/// Note that we don't want to add a `(?m)` in the beginning to change the
829/// whole regex because that would also change the meaning of `^`. In
830/// fancy-regex, that also matches at the end of e.g. `test\n` which is
831/// different from onig. It would also change `.` to match more.
832fn regex_for_newlines(regex: String) -> String {
833    if !regex.contains('$') {
834        return regex;
835    }
836
837    let rewriter = RegexRewriterForNewlines {
838        parser: Parser::new(regex.as_bytes()),
839    };
840    rewriter.rewrite()
841}
842
843struct RegexRewriterForNewlines<'a> {
844    parser: Parser<'a>,
845}
846
847impl RegexRewriterForNewlines<'_> {
848    fn rewrite(mut self) -> String {
849        let mut result = Vec::new();
850
851        while let Some(c) = self.parser.peek() {
852            match c {
853                b'$' => {
854                    self.parser.next();
855                    result.extend_from_slice(br"(?m:$)");
856                }
857                b'\\' => {
858                    self.parser.next();
859                    result.push(c);
860                    if let Some(c2) = self.parser.peek() {
861                        self.parser.next();
862                        result.push(c2);
863                    }
864                }
865                b'[' => {
866                    let (mut content, _) = self.parser.parse_character_class();
867                    result.append(&mut content);
868                }
869                _ => {
870                    self.parser.next();
871                    result.push(c);
872                }
873            }
874        }
875        String::from_utf8(result).unwrap()
876    }
877}
878
879/// Rewrite a regex that matches `\n` to one that matches `$` (end of line) instead.
880/// That allows the regex to be used to match lines that don't include a trailing newline character.
881///
882/// The reason we're doing this is because the regexes in the syntax definitions assume that the
883/// lines that are being matched on include a trailing newline.
884///
885/// Note that the rewrite is just an approximation and there's a couple of cases it can not handle,
886/// due to `$` being an anchor whereas `\n` matches a character.
887fn regex_for_no_newlines(regex: String) -> String {
888    if !regex.contains(r"\n") {
889        return regex;
890    }
891
892    // A special fix to rewrite a pattern from the `Rd` syntax that the RegexRewriter can not
893    // handle properly.
894    let regex = regex.replace("(?:\\n)?", "(?:$|)");
895
896    let rewriter = RegexRewriterForNoNewlines {
897        parser: Parser::new(regex.as_bytes()),
898    };
899    rewriter.rewrite()
900}
901
902struct RegexRewriterForNoNewlines<'a> {
903    parser: Parser<'a>,
904}
905
906impl RegexRewriterForNoNewlines<'_> {
907    fn rewrite(mut self) -> String {
908        let mut result = Vec::new();
909        while let Some(c) = self.parser.peek() {
910            match c {
911                b'\\' => {
912                    self.parser.next();
913                    if let Some(c2) = self.parser.peek() {
914                        self.parser.next();
915                        // Replacing `\n` with `$` in `\n?` or `\n+` would make parsing later fail
916                        // with "target of repeat operator is invalid"
917                        let c3 = self.parser.peek();
918                        if c2 == b'n' && c3 != Some(b'?') && c3 != Some(b'+') && c3 != Some(b'*') {
919                            result.extend_from_slice(b"$");
920                        } else {
921                            result.push(c);
922                            result.push(c2);
923                        }
924                    } else {
925                        result.push(c);
926                    }
927                }
928                b'[' => {
929                    let (mut content, matches_newline) = self.parser.parse_character_class();
930                    if matches_newline && self.parser.peek() != Some(b'?') {
931                        result.extend_from_slice(b"(?:");
932                        result.append(&mut content);
933                        result.extend_from_slice(br"|$)");
934                    } else {
935                        result.append(&mut content);
936                    }
937                }
938                _ => {
939                    self.parser.next();
940                    result.push(c);
941                }
942            }
943        }
944        String::from_utf8(result).unwrap()
945    }
946}
947
948fn get_consuming_capture_indexes(regex: &str) -> Vec<usize> {
949    let parser = ConsumingCaptureIndexParser {
950        parser: Parser::new(regex.as_bytes()),
951    };
952    parser.get_consuming_capture_indexes()
953}
954
955struct ConsumingCaptureIndexParser<'a> {
956    parser: Parser<'a>,
957}
958
959impl ConsumingCaptureIndexParser<'_> {
960    /// Find capture groups which are not inside lookarounds.
961    ///
962    /// If, in a YAML syntax definition, a scope stack is applied to a capture group inside a
963    /// lookaround, (i.e. "captures:\n x: scope.stack goes.here", where "x" is the number of a
964    /// capture group in a lookahead/behind), those those scopes are not applied, so no need to
965    /// even parse them.
966    fn get_consuming_capture_indexes(mut self) -> Vec<usize> {
967        let mut result = Vec::new();
968        let mut stack = Vec::new();
969        let mut cap_num = 0;
970        let mut in_lookaround = false;
971        stack.push(in_lookaround);
972        result.push(cap_num);
973
974        while let Some(c) = self.parser.peek() {
975            match c {
976                b'\\' => {
977                    self.parser.next();
978                    self.parser.next();
979                }
980                b'[' => {
981                    self.parser.parse_character_class();
982                }
983                b'(' => {
984                    self.parser.next();
985                    // add the current lookaround state to the stack so we can just pop at a closing paren
986                    stack.push(in_lookaround);
987                    if let Some(c2) = self.parser.peek() {
988                        if c2 != b'?' {
989                            // simple numbered capture group
990                            cap_num += 1;
991                            // if we are not currently in a lookaround,
992                            // add this capture group number to the valid ones
993                            if !in_lookaround {
994                                result.push(cap_num);
995                            }
996                        } else {
997                            self.parser.next();
998                            if let Some(c3) = self.parser.peek() {
999                                self.parser.next();
1000                                if c3 == b'=' || c3 == b'!' {
1001                                    // lookahead
1002                                    in_lookaround = true;
1003                                } else if c3 == b'<' {
1004                                    if let Some(c4) = self.parser.peek() {
1005                                        if c4 == b'=' || c4 == b'!' {
1006                                            self.parser.next();
1007                                            // lookbehind
1008                                            in_lookaround = true;
1009                                        }
1010                                    }
1011                                } else if c3 == b'P' {
1012                                    if let Some(c4) = self.parser.peek() {
1013                                        if c4 == b'<' {
1014                                            // named capture group
1015                                            cap_num += 1;
1016                                            // if we are not currently in a lookaround,
1017                                            // add this capture group number to the valid ones
1018                                            if !in_lookaround {
1019                                                result.push(cap_num);
1020                                            }
1021                                        }
1022                                    }
1023                                }
1024                            }
1025                        }
1026                    }
1027                }
1028                b')' => {
1029                    if let Some(value) = stack.pop() {
1030                        in_lookaround = value;
1031                    }
1032                    self.parser.next();
1033                }
1034                _ => {
1035                    self.parser.next();
1036                }
1037            }
1038        }
1039        result
1040    }
1041}
1042
1043struct Parser<'a> {
1044    bytes: &'a [u8],
1045    index: usize,
1046}
1047
1048impl Parser<'_> {
1049    fn new(bytes: &[u8]) -> Parser<'_> {
1050        Parser { bytes, index: 0 }
1051    }
1052
1053    fn peek(&self) -> Option<u8> {
1054        self.bytes.get(self.index).copied()
1055    }
1056
1057    fn next(&mut self) {
1058        self.index += 1;
1059    }
1060
1061    fn parse_character_class(&mut self) -> (Vec<u8>, bool) {
1062        let mut content = Vec::new();
1063        let mut negated = false;
1064        let mut nesting = 0;
1065        let mut matches_newline = false;
1066
1067        self.next();
1068        content.push(b'[');
1069        if let Some(b'^') = self.peek() {
1070            self.next();
1071            content.push(b'^');
1072            negated = true;
1073        }
1074
1075        // An unescaped `]` is allowed after `[` or `[^` and doesn't mean the end of the class.
1076        if let Some(b']') = self.peek() {
1077            self.next();
1078            content.push(b']');
1079        }
1080
1081        while let Some(c) = self.peek() {
1082            match c {
1083                b'\\' => {
1084                    self.next();
1085                    content.push(c);
1086                    if let Some(c2) = self.peek() {
1087                        self.next();
1088                        if c2 == b'n' && !negated && nesting == 0 {
1089                            matches_newline = true;
1090                        }
1091                        content.push(c2);
1092                    }
1093                }
1094                b'[' => {
1095                    self.next();
1096                    content.push(b'[');
1097                    nesting += 1;
1098                }
1099                b']' => {
1100                    self.next();
1101                    content.push(b']');
1102                    if nesting == 0 {
1103                        break;
1104                    }
1105                    nesting -= 1;
1106                }
1107                _ => {
1108                    self.next();
1109                    content.push(c);
1110                }
1111            }
1112        }
1113
1114        (content, matches_newline)
1115    }
1116}
1117
1118#[cfg(test)]
1119mod tests {
1120    use super::*;
1121    use crate::parsing::Scope;
1122
1123    #[test]
1124    fn can_parse() {
1125        let defn: SyntaxDefinition = SyntaxDefinition::load_from_str(
1126            "name: C\nscope: source.c\ncontexts: {main: []}",
1127            false,
1128            None,
1129        )
1130        .unwrap();
1131        assert_eq!(defn.name, "C");
1132        assert_eq!(defn.scope, Scope::new("source.c").unwrap());
1133        let exts_empty: Vec<String> = Vec::new();
1134        assert_eq!(defn.file_extensions, exts_empty);
1135        assert!(!defn.hidden);
1136        assert!(defn.variables.is_empty());
1137        let defn2: SyntaxDefinition = SyntaxDefinition::load_from_str(
1138            "
1139        name: C
1140        scope: source.c
1141        file_extensions: [c, h]
1142        hidden_file_extensions: [k, l]
1143        hidden: true
1144        variables:
1145          ident: '[QY]+'
1146        contexts:
1147          prototype:
1148            - match: lol
1149              scope: source.php
1150          main:
1151            - match: \\b(if|else|for|while|{{ident}})\\b
1152              scope: keyword.control.c keyword.looping.c
1153              captures:
1154                  1: meta.preprocessor.c++
1155                  2: keyword.control.include.c++
1156              push: [string, 'scope:source.c#main', 'CSS.sublime-syntax#rule-list-body']
1157              with_prototype:
1158                - match: wow
1159                  pop: true
1160            - match: '\"'
1161              push: string
1162          string:
1163            - meta_scope: string.quoted.double.c
1164            - meta_include_prototype: false
1165            - match: \\\\.
1166              scope: constant.character.escape.c
1167            - match: '\"'
1168              pop: true
1169        ",
1170            false,
1171            None,
1172        )
1173        .unwrap();
1174        assert_eq!(defn2.name, "C");
1175        let top_level_scope = Scope::new("source.c").unwrap();
1176        assert_eq!(defn2.scope, top_level_scope);
1177        let exts: Vec<String> = vec!["c", "h", "k", "l"]
1178            .into_iter()
1179            .map(String::from)
1180            .collect();
1181        assert_eq!(defn2.file_extensions, exts);
1182        assert!(defn2.hidden);
1183        assert_eq!(defn2.variables.get("ident").unwrap(), "[QY]+");
1184
1185        let n: Vec<Scope> = Vec::new();
1186        println!("{:?}", defn2);
1187        // unreachable!();
1188        let main = &defn2.contexts["main"];
1189        assert_eq!(main.meta_content_scope, vec![top_level_scope]);
1190        assert_eq!(main.meta_scope, n);
1191        assert!(main.meta_include_prototype.unwrap_or(true));
1192
1193        assert_eq!(defn2.contexts["__main"].meta_content_scope, n);
1194        assert_eq!(
1195            defn2.contexts["__start"].meta_content_scope,
1196            vec![top_level_scope]
1197        );
1198
1199        assert_eq!(
1200            defn2.contexts["string"].meta_scope,
1201            vec![Scope::new("string.quoted.double.c").unwrap()]
1202        );
1203        let first_pattern: &Pattern = &main.patterns[0];
1204        match *first_pattern {
1205            Pattern::Match(ref match_pat) => {
1206                let m: &CaptureMapping = match_pat.captures.as_ref().expect("test failed");
1207                assert_eq!(
1208                    &m[0],
1209                    &(1, vec![Scope::new("meta.preprocessor.c++").unwrap()])
1210                );
1211                use crate::parsing::syntax_definition::ContextReference::*;
1212
1213                // this is sadly necessary because Context is not Eq because of the Regex
1214                let expected = MatchOperation::Push(vec![
1215                    Named("string".to_owned()),
1216                    ByScope {
1217                        scope: Scope::new("source.c").unwrap(),
1218                        sub_context: Some("main".to_owned()),
1219                        with_escape: false,
1220                    },
1221                    File {
1222                        name: "CSS".to_owned(),
1223                        sub_context: Some("rule-list-body".to_owned()),
1224                        with_escape: false,
1225                    },
1226                ]);
1227                assert_eq!(
1228                    format!("{:?}", match_pat.operation),
1229                    format!("{:?}", expected)
1230                );
1231
1232                assert_eq!(
1233                    match_pat.scope,
1234                    vec![
1235                        Scope::new("keyword.control.c").unwrap(),
1236                        Scope::new("keyword.looping.c").unwrap()
1237                    ]
1238                );
1239
1240                assert!(match_pat.with_prototype.is_some());
1241            }
1242            _ => unreachable!(),
1243        }
1244    }
1245
1246    #[test]
1247    fn can_parse_embed_produces_embed_op() {
1248        let def = SyntaxDefinition::load_from_str(
1249            r#"
1250        name: C
1251        scope: source.c
1252        file_extensions: [c, h]
1253        variables:
1254          ident: '[QY]+'
1255        contexts:
1256          main:
1257            - match: '(>)\s*'
1258              captures:
1259                1: meta.tag.style.begin.html punctuation.definition.tag.end.html
1260              embed: scope:source.css
1261              embed_scope: source.css.embedded.html
1262              escape: (?i)(?=</style)
1263        "#,
1264            false,
1265            None,
1266        )
1267        .unwrap();
1268
1269        // Verify the operation is Embed (not Push)
1270        let main_ctx = &def.contexts["main"];
1271        if let Pattern::Match(ref match_pattern) = main_ctx.patterns[0] {
1272            match match_pattern.operation {
1273                MatchOperation::Embed {
1274                    ref contexts,
1275                    ref escape,
1276                    pop_count,
1277                } => {
1278                    assert_eq!(pop_count, 0);
1279                    // Should have 2 contexts: wrapper for embed_scope + the embedded syntax
1280                    assert_eq!(contexts.len(), 2);
1281                    // First is the inline wrapper context
1282                    assert!(matches!(contexts[0], ContextReference::Inline(_)));
1283                    // Second is the scope reference
1284                    assert!(matches!(
1285                        contexts[1],
1286                        ContextReference::ByScope {
1287                            with_escape: true,
1288                            ..
1289                        }
1290                    ));
1291                    // Escape regex should be present
1292                    assert_eq!(escape.escape_regex.regex_str(), "(?i)(?=</style)");
1293                    assert!(!escape.has_captures);
1294                    assert!(escape.escape_captures.is_none());
1295                }
1296                _ => panic!(
1297                    "Expected Embed operation, got {:?}",
1298                    match_pattern.operation
1299                ),
1300            }
1301            // No with_prototype for embed (escape is native)
1302            assert!(match_pattern.with_prototype.is_none());
1303        } else {
1304            panic!("Expected Match pattern");
1305        }
1306    }
1307
1308    #[test]
1309    fn errors_on_embed_without_escape() {
1310        let def = SyntaxDefinition::load_from_str(
1311            r#"
1312        name: C
1313        scope: source.c
1314        file_extensions: [c, h]
1315        variables:
1316          ident: '[QY]+'
1317        contexts:
1318          main:
1319            - match: '(>)\s*'
1320              captures:
1321                1: meta.tag.style.begin.html punctuation.definition.tag.end.html
1322              embed: scope:source.css
1323              embed_scope: source.css.embedded.html
1324        "#,
1325            false,
1326            None,
1327        );
1328        assert!(def.is_err());
1329        match def.unwrap_err() {
1330            ParseSyntaxError::MissingMandatoryKey(key) => assert_eq!(key, "escape"),
1331            _ => unreachable!("Got unexpected ParseSyntaxError"),
1332        }
1333    }
1334
1335    #[test]
1336    fn can_parse_pop_plus_embed() {
1337        let def = SyntaxDefinition::load_from_str(
1338            r#"
1339        name: Test
1340        scope: text.test
1341        file_extensions: [test]
1342        contexts:
1343          main:
1344            - match: '<script>'
1345              push: script-content
1346          script-content:
1347            - match: '>'
1348              pop: 1
1349              embed: scope:source.js
1350              embed_scope: source.js.embedded.html
1351              escape: '(?=</script>)'
1352        "#,
1353            false,
1354            None,
1355        )
1356        .unwrap();
1357
1358        let ctx = &def.contexts["script-content"];
1359        if let Pattern::Match(ref match_pattern) = ctx.patterns[0] {
1360            match match_pattern.operation {
1361                MatchOperation::Embed {
1362                    ref contexts,
1363                    ref escape,
1364                    pop_count,
1365                } => {
1366                    assert_eq!(pop_count, 1);
1367                    // 2 contexts: embed_scope wrapper + scope reference
1368                    assert_eq!(contexts.len(), 2);
1369                    assert!(matches!(contexts[0], ContextReference::Inline(_)));
1370                    assert!(matches!(
1371                        contexts[1],
1372                        ContextReference::ByScope {
1373                            with_escape: true,
1374                            ..
1375                        }
1376                    ));
1377                    assert_eq!(escape.escape_regex.regex_str(), "(?=</script>)");
1378                }
1379                _ => panic!(
1380                    "Expected Embed operation, got {:?}",
1381                    match_pattern.operation
1382                ),
1383            }
1384        } else {
1385            panic!("Expected Match pattern");
1386        }
1387    }
1388
1389    #[test]
1390    fn errors_on_regex_compile_error() {
1391        let def = SyntaxDefinition::load_from_str(
1392            r#"
1393        name: C
1394        scope: source.c
1395        file_extensions: [test]
1396        contexts:
1397          main:
1398            - match: '[a'
1399              scope: keyword.name
1400        "#,
1401            false,
1402            None,
1403        );
1404        assert!(def.is_err());
1405        match def.unwrap_err() {
1406            ParseSyntaxError::RegexCompileError(ref regex, _) => assert_eq!("[a", regex),
1407            _ => unreachable!("Got unexpected ParseSyntaxError"),
1408        }
1409    }
1410
1411    #[test]
1412    fn can_parse_ugly_yaml() {
1413        let defn: SyntaxDefinition = SyntaxDefinition::load_from_str(
1414            "
1415        name: LaTeX
1416        scope: text.tex.latex
1417        contexts:
1418          main:
1419            - match: '((\\\\)(?:framebox|makebox))\\b'
1420              captures:
1421                1: support.function.box.latex
1422                2: punctuation.definition.backslash.latex
1423              push:
1424                - [{meta_scope: meta.function.box.latex}, {match: '', pop: true}]
1425                - argument
1426                - optional-arguments
1427          argument:
1428            - match: '\\{'
1429              scope: punctuation.definition.group.brace.begin.latex
1430            - match: '(?=\\S)'
1431              pop: true
1432          optional-arguments:
1433            - match: '(?=\\S)'
1434              pop: true
1435        ",
1436            false,
1437            None,
1438        )
1439        .unwrap();
1440        assert_eq!(defn.name, "LaTeX");
1441        let top_level_scope = Scope::new("text.tex.latex").unwrap();
1442        assert_eq!(defn.scope, top_level_scope);
1443
1444        let first_pattern: &Pattern = &defn.contexts["main"].patterns[0];
1445        match *first_pattern {
1446            Pattern::Match(ref match_pat) => {
1447                let m: &CaptureMapping = match_pat.captures.as_ref().expect("test failed");
1448                assert_eq!(
1449                    &m[0],
1450                    &(1, vec![Scope::new("support.function.box.latex").unwrap()])
1451                );
1452
1453                //use parsing::syntax_definition::ContextReference::*;
1454                // TODO: check the first pushed reference is Inline(...) and has a meta_scope of meta.function.box.latex
1455                // TODO: check the second pushed reference is Named("argument".to_owned())
1456                // TODO: check the third pushed reference is Named("optional-arguments".to_owned())
1457
1458                assert!(match_pat.with_prototype.is_none());
1459            }
1460            _ => unreachable!(),
1461        }
1462    }
1463
1464    #[test]
1465    fn names_anonymous_contexts() {
1466        let def = SyntaxDefinition::load_from_str(
1467            r#"
1468            scope: source.c
1469            contexts:
1470              main:
1471                - match: a
1472                  push: a
1473              a:
1474                - meta_scope: a
1475                - match: x
1476                  push:
1477                    - meta_scope: anonymous_x
1478                    - match: anything
1479                      push:
1480                        - meta_scope: anonymous_x_2
1481                - match: y
1482                  push:
1483                    - meta_scope: anonymous_y
1484                - match: z
1485                  escape: 'test'
1486            "#,
1487            false,
1488            None,
1489        )
1490        .unwrap();
1491
1492        assert_eq!(def.contexts["a"].meta_scope, vec![Scope::new("a").unwrap()]);
1493        assert_eq!(
1494            def.contexts["#anon_a_0"].meta_scope,
1495            vec![Scope::new("anonymous_x").unwrap()]
1496        );
1497        assert_eq!(
1498            def.contexts["#anon_a_1"].meta_scope,
1499            vec![Scope::new("anonymous_x_2").unwrap()]
1500        );
1501        assert_eq!(
1502            def.contexts["#anon_a_2"].meta_scope,
1503            vec![Scope::new("anonymous_y").unwrap()]
1504        );
1505        // With native embed/escape, no synthetic escape context is created,
1506        // so #anon_a_3 should not exist.
1507        assert!(!def.contexts.contains_key("#anon_a_3"));
1508    }
1509
1510    #[test]
1511    fn can_use_fallback_name() {
1512        let def = SyntaxDefinition::load_from_str(
1513            r#"
1514        scope: source.c
1515        contexts:
1516          main:
1517            - match: ''
1518        "#,
1519            false,
1520            Some("C"),
1521        );
1522        assert_eq!(def.unwrap().name, "C");
1523    }
1524
1525    #[test]
1526    fn can_rewrite_regex_for_newlines() {
1527        fn rewrite(s: &str) -> String {
1528            regex_for_newlines(s.to_string())
1529        }
1530
1531        assert_eq!(&rewrite(r"a"), r"a");
1532        assert_eq!(&rewrite(r"\b"), r"\b");
1533        assert_eq!(&rewrite(r"(a)"), r"(a)");
1534        assert_eq!(&rewrite(r"[a]"), r"[a]");
1535        assert_eq!(&rewrite(r"[^a]"), r"[^a]");
1536        assert_eq!(&rewrite(r"[]a]"), r"[]a]");
1537        assert_eq!(&rewrite(r"[[a]]"), r"[[a]]");
1538
1539        assert_eq!(&rewrite(r"^"), r"^");
1540        assert_eq!(&rewrite(r"$"), r"(?m:$)");
1541        assert_eq!(&rewrite(r"^ab$"), r"^ab(?m:$)");
1542        assert_eq!(&rewrite(r"\^ab\$"), r"\^ab\$");
1543        assert_eq!(&rewrite(r"(//).*$"), r"(//).*(?m:$)");
1544
1545        // Do not rewrite this `$` because it's in a char class and doesn't mean end of line
1546        assert_eq!(&rewrite(r"[a$]"), r"[a$]");
1547    }
1548
1549    #[test]
1550    fn can_rewrite_regex_for_no_newlines() {
1551        fn rewrite(s: &str) -> String {
1552            regex_for_no_newlines(s.to_string())
1553        }
1554
1555        assert_eq!(&rewrite(r"a"), r"a");
1556        assert_eq!(&rewrite(r"\b"), r"\b");
1557        assert_eq!(&rewrite(r"(a)"), r"(a)");
1558        assert_eq!(&rewrite(r"[a]"), r"[a]");
1559        assert_eq!(&rewrite(r"[^a]"), r"[^a]");
1560        assert_eq!(&rewrite(r"[]a]"), r"[]a]");
1561        assert_eq!(&rewrite(r"[[a]]"), r"[[a]]");
1562
1563        assert_eq!(&rewrite(r"\n"), r"$");
1564        assert_eq!(&rewrite(r"\[\n"), r"\[$");
1565        assert_eq!(&rewrite(r"a\n?"), r"a\n?");
1566        assert_eq!(&rewrite(r"a\n+"), r"a\n+");
1567        assert_eq!(&rewrite(r"a\n*"), r"a\n*");
1568        assert_eq!(&rewrite(r"[abc\n]"), r"(?:[abc\n]|$)");
1569        assert_eq!(&rewrite(r"[^\n]"), r"[^\n]");
1570        assert_eq!(&rewrite(r"[^]\n]"), r"[^]\n]");
1571        assert_eq!(&rewrite(r"[\n]?"), r"[\n]?");
1572        // Removing the `\n` might result in an empty character class, so we should leave it.
1573        assert_eq!(&rewrite(r"[\n]"), r"(?:[\n]|$)");
1574        assert_eq!(&rewrite(r"[]\n]"), r"(?:[]\n]|$)");
1575        // In order to properly understand nesting, we'd have to have a full parser, so ignore it.
1576        assert_eq!(&rewrite(r"[[a]&&[\n]]"), r"[[a]&&[\n]]");
1577
1578        assert_eq!(&rewrite(r"ab(?:\n)?"), r"ab(?:$|)");
1579        assert_eq!(&rewrite(r"(?<!\n)ab"), r"(?<!$)ab");
1580        assert_eq!(&rewrite(r"(?<=\n)ab"), r"(?<=$)ab");
1581    }
1582
1583    #[test]
1584    fn can_get_valid_captures_from_regex() {
1585        let regex = "hello(test)(?=(world))(foo(?P<named>bar))";
1586        println!("{:?}", regex);
1587        let valid_indexes = get_consuming_capture_indexes(regex);
1588        println!("{:?}", valid_indexes);
1589        assert_eq!(valid_indexes, [0, 1, 3, 4]);
1590    }
1591
1592    #[test]
1593    fn can_get_valid_captures_from_regex2() {
1594        let regex = "hello(test)[(?=tricked](foo(bar))";
1595        println!("{:?}", regex);
1596        let valid_indexes = get_consuming_capture_indexes(regex);
1597        println!("{:?}", valid_indexes);
1598        assert_eq!(valid_indexes, [0, 1, 2, 3]);
1599    }
1600
1601    #[test]
1602    fn can_get_valid_captures_from_nested_regex() {
1603        let regex = "hello(test)(?=(world(?!(te(?<=(st))))))(foo(bar))";
1604        println!("{:?}", regex);
1605        let valid_indexes = get_consuming_capture_indexes(regex);
1606        println!("{:?}", valid_indexes);
1607        assert_eq!(valid_indexes, [0, 1, 5, 6]);
1608    }
1609
1610    #[test]
1611    fn error_loading_syntax_with_unescaped_backslash() {
1612        let load_err = SyntaxDefinition::load_from_str(
1613            r#"
1614            name: Unescaped Backslash
1615            scope: source.c
1616            file_extensions: [test]
1617            contexts:
1618              main:
1619                - match: '\'
1620            "#,
1621            false,
1622            None,
1623        )
1624        .unwrap_err();
1625        match load_err {
1626            ParseSyntaxError::RegexCompileError(bad_regex, _) => assert_eq!(bad_regex, r"\"),
1627            _ => panic!("Unexpected error: {load_err}"),
1628        }
1629    }
1630
1631    #[test]
1632    fn can_parse_extends_field() {
1633        let defn = SyntaxDefinition::load_from_str(
1634            r#"
1635            name: C++
1636            scope: source.c++
1637            extends: Packages/C/C.sublime-syntax
1638            contexts:
1639              main:
1640                - match: 'class'
1641                  scope: keyword.c++
1642            "#,
1643            false,
1644            None,
1645        )
1646        .unwrap();
1647        assert_eq!(defn.extends, vec!["Packages/C/C.sublime-syntax".to_owned()]);
1648    }
1649
1650    #[test]
1651    fn can_parse_extends_without_main() {
1652        // A child with extends can omit the main context
1653        let defn = SyntaxDefinition::load_from_str(
1654            r#"
1655            name: C++ Extra
1656            scope: source.c++
1657            extends: Packages/C/C.sublime-syntax
1658            contexts:
1659              extra:
1660                - match: 'extra'
1661                  scope: keyword.extra
1662            "#,
1663            false,
1664            None,
1665        )
1666        .unwrap();
1667        assert_eq!(defn.extends, vec!["Packages/C/C.sublime-syntax".to_owned()]);
1668        assert!(!defn.contexts.contains_key("main"));
1669    }
1670
1671    #[test]
1672    fn can_parse_version_field() {
1673        let defn = SyntaxDefinition::load_from_str(
1674            r#"
1675            name: V2 Test
1676            scope: source.v2
1677            version: 2
1678            contexts:
1679              main:
1680                - match: 'test'
1681                  scope: keyword.test
1682            "#,
1683            false,
1684            None,
1685        )
1686        .unwrap();
1687        assert_eq!(defn.version, 2);
1688    }
1689
1690    #[test]
1691    fn version_defaults_to_1() {
1692        let defn = SyntaxDefinition::load_from_str(
1693            "name: V1\nscope: source.v1\ncontexts: {main: []}",
1694            false,
1695            None,
1696        )
1697        .unwrap();
1698        assert_eq!(defn.version, 1);
1699    }
1700
1701    #[test]
1702    fn can_parse_meta_prepend() {
1703        let defn = SyntaxDefinition::load_from_str(
1704            r#"
1705            name: Test
1706            scope: source.test
1707            extends: Packages/Base/Base.sublime-syntax
1708            contexts:
1709              main:
1710                - meta_prepend: true
1711                - match: 'prepended'
1712                  scope: keyword.prepended
1713            "#,
1714            false,
1715            None,
1716        )
1717        .unwrap();
1718        let main = &defn.contexts["main"];
1719        assert_eq!(main.merge_mode, ContextMergeMode::Prepend);
1720    }
1721
1722    #[test]
1723    fn can_parse_meta_append() {
1724        let defn = SyntaxDefinition::load_from_str(
1725            r#"
1726            name: Test
1727            scope: source.test
1728            extends: Packages/Base/Base.sublime-syntax
1729            contexts:
1730              main:
1731                - meta_append: true
1732                - match: 'appended'
1733                  scope: keyword.appended
1734            "#,
1735            false,
1736            None,
1737        )
1738        .unwrap();
1739        let main = &defn.contexts["main"];
1740        assert_eq!(main.merge_mode, ContextMergeMode::Append);
1741    }
1742
1743    #[test]
1744    fn can_parse_apply_prototype() {
1745        let defn = SyntaxDefinition::load_from_str(
1746            r#"
1747            name: Test
1748            scope: source.test
1749            contexts:
1750              main:
1751                - include: scope:source.other
1752                  apply_prototype: true
1753            "#,
1754            false,
1755            None,
1756        )
1757        .unwrap();
1758        let main = &defn.contexts["main"];
1759        assert_eq!(main.patterns.len(), 1);
1760        match &main.patterns[0] {
1761            Pattern::IncludeWithPrototype(_) => {}
1762            other => panic!("Expected IncludeWithPrototype, got {:?}", other),
1763        }
1764    }
1765
1766    #[test]
1767    fn stores_raw_regex_str() {
1768        let defn = SyntaxDefinition::load_from_str(
1769            r#"
1770            name: Test
1771            scope: source.test
1772            variables:
1773              ident: '[a-z]+'
1774            contexts:
1775              main:
1776                - match: '{{ident}}'
1777                  scope: variable.test
1778            "#,
1779            false,
1780            None,
1781        )
1782        .unwrap();
1783        let main = &defn.contexts["main"];
1784        match &main.patterns[0] {
1785            Pattern::Match(mp) => {
1786                assert_eq!(mp.raw_regex_str.as_deref(), Some("{{ident}}"));
1787            }
1788            _ => panic!("Expected Match pattern"),
1789        }
1790    }
1791}