syntect-tmlanguage 0.1.0

Load real TextMate .tmLanguage.json grammars into syntect's own SyntaxDefinition data model
Documentation
//! `applyEndPatternLast` changes whether `end` is tried before or after the
//! body's own patterns at each position. Uses a deliberately ambiguous
//! grammar — a body pattern that also matches the `end` delimiter — so the
//! two settings produce genuinely different, observable behavior through
//! `syntect`'s own `ParseState` (not just different generated YAML text):
//! with the default, `>` closes the tag; with `applyEndPatternLast: true`,
//! the body's own catch-all pattern wins instead and the tag never closes.

use syntect::parsing::{Scope, ScopeStack, SyntaxSetBuilder};

fn tag_left_open_after(source: &str, apply_end_pattern_last: bool) -> bool {
    let grammar = format!(
        r#"{{
            "scopeName": "source.test",
            "patterns": [
                {{
                    "begin": "<",
                    "end": ">",
                    "name": "meta.tag.test",
                    "applyEndPatternLast": {apply_end_pattern_last},
                    "patterns": [
                        {{ "match": ".", "name": "text.body.test" }}
                    ]
                }}
            ]
        }}"#
    );

    let definition = syntect_tmlanguage::load(&grammar).expect("translation failed");
    let mut builder = SyntaxSetBuilder::new();
    builder.add(definition);
    let syntax_set = builder.build();
    let syntax = syntax_set
        .find_syntax_by_scope(Scope::new("source.test").unwrap())
        .unwrap();

    let mut parse_state = syntect::parsing::ParseState::new(syntax);
    let mut scope_stack = ScopeStack::new();
    for line in source.lines() {
        let ops = parse_state.parse_line(line, &syntax_set).unwrap();
        for (_, op) in ops {
            scope_stack.apply(&op).unwrap();
        }
    }
    scope_stack
        .as_slice()
        .iter()
        .any(|s| s.build_string() == "meta.tag.test")
}

#[test]
fn default_end_pattern_is_tried_before_body_patterns() {
    // '>' is checked against `end` first (TextMate's default), pops the
    // tag, so nothing is left open.
    assert!(
        !tag_left_open_after("<a>b", false),
        "tag should have closed on '>' with the default ordering"
    );
}

#[test]
fn apply_end_pattern_last_tries_body_patterns_before_end() {
    // The body's own catch-all `.` now wins over `end` at '>' (it's tried
    // first), so `>` is consumed as ordinary body content and the tag
    // never closes.
    assert!(
        tag_left_open_after("<a>b", true),
        "tag should still be open — its own body pattern should have consumed '>' before `end` got a chance"
    );
}