syntect-tmlanguage 0.1.0

Load real TextMate .tmLanguage.json grammars into syntect's own SyntaxDefinition data model
Documentation
//! Deserialization model for `.tmLanguage.json` grammars.
//!
//! Mirrors the shape TextMate/VS Code grammars are actually authored in,
//! not `syntect`'s own `SyntaxDefinition` model — see [`crate::translate`]
//! for the conversion between the two.

use std::collections::HashMap;

use serde::Deserialize;

#[derive(Debug, Clone, Deserialize)]
pub struct RawGrammar {
    #[serde(rename = "scopeName")]
    pub scope_name: String,
    pub name: Option<String>,
    #[serde(rename = "fileTypes", default)]
    pub file_types: Vec<String>,
    #[serde(rename = "firstLineMatch")]
    pub first_line_match: Option<String>,
    #[serde(default)]
    pub patterns: Vec<RawRule>,
    #[serde(default)]
    pub repository: HashMap<String, RawRule>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct RawRule {
    pub include: Option<String>,

    #[serde(rename = "match")]
    pub match_: Option<String>,
    pub name: Option<String>,
    pub captures: Option<HashMap<String, RawCapture>>,

    pub begin: Option<String>,
    #[serde(rename = "beginCaptures")]
    pub begin_captures: Option<HashMap<String, RawCapture>>,
    pub end: Option<String>,
    #[serde(rename = "endCaptures")]
    pub end_captures: Option<HashMap<String, RawCapture>>,
    /// `begin`/`while` rules are real (VS Code's own `markdown.tmLanguage.json`
    /// uses them extensively, e.g. for blockquotes and fenced code blocks) —
    /// but `syntect` has no engine-level equivalent at all (no YAML `while:`
    /// key, no `MatchOperation` variant for it), so this field exists only
    /// to detect and reject them with a clear error rather than silently
    /// mistranslating. See [`crate::translate::Error::WhileNotSupported`].
    #[serde(rename = "while")]
    pub while_: Option<String>,
    #[serde(rename = "contentName")]
    pub content_name: Option<String>,
    #[serde(
        rename = "applyEndPatternLast",
        default,
        deserialize_with = "lenient_bool"
    )]
    pub apply_end_pattern_last: bool,

    #[serde(default)]
    pub patterns: Vec<RawRule>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct RawCapture {
    pub name: Option<String>,
}

impl RawGrammar {
    pub fn from_json(source: &str) -> Result<Self, serde_json::Error> {
        serde_json::from_str(source)
    }
}

/// Real-world grammars aren't always strict about `applyEndPatternLast`
/// being a JSON boolean — e.g. VS Code's own `shell-unix-bash.tmLanguage.json`
/// has `"applyEndPatternLast": 1`. Accept a number as truthy/falsy too,
/// matching how such grammars are actually authored and how a JS-based
/// consumer would treat the value.
fn lenient_bool<'de, D>(deserializer: D) -> Result<bool, D::Error>
where
    D: serde::Deserializer<'de>,
{
    struct LenientBoolVisitor;

    impl serde::de::Visitor<'_> for LenientBoolVisitor {
        type Value = bool;

        fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            f.write_str("a boolean, or a truthy/falsy number")
        }

        fn visit_bool<E>(self, v: bool) -> Result<bool, E> {
            Ok(v)
        }

        fn visit_u64<E>(self, v: u64) -> Result<bool, E> {
            Ok(v != 0)
        }

        fn visit_i64<E>(self, v: i64) -> Result<bool, E> {
            Ok(v != 0)
        }

        fn visit_f64<E>(self, v: f64) -> Result<bool, E> {
            Ok(v != 0.0)
        }
    }

    deserializer.deserialize_any(LenientBoolVisitor)
}