rossi-cli 0.1.0

Command-line interface for the Rossi Event-B toolchain
//! Sublime Text syntax (`EventB.sublime-syntax`).
//!
//! Whole-file. Sublime's format is a regex/scope superset of TextMate and is
//! read by the `syntect` library, so this one file gives **bat** and **delta**
//! (and Sublime Text itself) Event-B highlighting — none of which run an LSP.

use super::Model;

const NOTICE: &str =
    "Generated by `rossi gen-grammars` from the canonical token tables. Do not edit by hand.";

/// Render the complete `EventB.sublime-syntax` document.
pub fn render(model: &Model) -> String {
    let mut out = String::new();
    out.push_str("%YAML 1.2\n---\n");
    out.push_str(&format!("# {NOTICE}\n"));
    out.push_str("name: Event-B\n");
    out.push_str("file_extensions:\n  - eventb\n");
    out.push_str("scope: source.eventb\n\n");
    out.push_str("contexts:\n");
    out.push_str("  main:\n");

    // Comments and strings first (so `//`, `/*`, `"` beat the operator rules).
    rule(&mut out, "//.*$", "comment.line.double-slash.eventb");
    push_match(
        &mut out,
        "/\\*",
        Some("punctuation.definition.comment.eventb"),
        Some("block_comment"),
    );
    push_match(
        &mut out,
        "\"",
        Some("punctuation.definition.string.begin.eventb"),
        Some("string"),
    );

    // Generated token groups, in model order (Word and Symbol both render as a
    // single `match`/`scope` rule; the regex already encodes the difference).
    for group in &model.groups {
        rule(&mut out, &group.regex_oniguruma(), group.scope.textmate());
    }

    // Numbers, labels, identifiers last.
    rule(&mut out, "\\b[0-9]+\\b", "constant.numeric.eventb");
    rule(&mut out, "@[A-Za-z0-9_]+", "entity.name.tag.eventb");
    rule(
        &mut out,
        "[a-zA-Z_][a-zA-Z0-9_]*'?",
        "variable.other.eventb",
    );

    // Block comment and string sub-contexts.
    out.push_str("  block_comment:\n");
    out.push_str("    - meta_scope: comment.block.eventb\n");
    push_pop(&mut out, "\\*/");
    out.push_str("  string:\n");
    out.push_str("    - meta_scope: string.quoted.double.eventb\n");
    rule(&mut out, "\\\\.", "constant.character.escape.eventb");
    push_pop(&mut out, "\"");

    out
}

/// `- match: '…'` / `scope: …`.
fn rule(out: &mut String, regex: &str, scope: &str) {
    out.push_str(&format!("    - match: {}\n", squote(regex)));
    out.push_str(&format!("      scope: {scope}\n"));
}

/// `- match: '…'` with an optional scope and an optional `push:` context.
fn push_match(out: &mut String, regex: &str, scope: Option<&str>, push: Option<&str>) {
    out.push_str(&format!("    - match: {}\n", squote(regex)));
    if let Some(s) = scope {
        out.push_str(&format!("      scope: {s}\n"));
    }
    if let Some(p) = push {
        out.push_str(&format!("      push: {p}\n"));
    }
}

/// `- match: '…'` / `pop: true`.
fn push_pop(out: &mut String, regex: &str) {
    out.push_str(&format!("    - match: {}\n", squote(regex)));
    out.push_str("      pop: true\n");
}

/// Single-quote a YAML scalar, doubling any embedded single quote.
fn squote(s: &str) -> String {
    format!("'{}'", s.replace('\'', "''"))
}