Skip to main content

textmate_lang/
lib.rs

1//! # textmate_lang
2//!
3//! Generate [TextMate grammars](https://macromates.com/manual/en/language_grammars)
4//! (`tmLanguage.json`) from Rust: describe a language's tokens with
5//! [`Grammar`] and [`Rule`]s, and emit the JSON that VS Code, Sublime Text,
6//! TextMate, Shiki, and other editors and highlighters load to highlight it,
7//! no tree-sitter required.
8//!
9//! Emitting validates everything a grammar file cannot check for itself.
10//! Every regular expression is held to a documented subset of Oniguruma
11//! syntax, and refused with a byte offset and a reason when it strays
12//! outside. Scope names, scope selectors, captures, and includes are checked;
13//! includes that would send a tokenizer into endless recursion are found.
14//! The JSON writer is the crate's own and escapes every string it writes, so
15//! no name, pattern, or scope can change the structure of the output.
16//!
17//! ## The pieces
18//!
19//! - [`Grammar`] — the scope name, top-level patterns, the repository of
20//!   named rules, and injections. [`Grammar::to_json`] validates it and
21//!   returns the JSON.
22//! - [`Rule`] — an entry of a pattern list: a [`Match`], a [`BeginEnd`] or
23//!   [`BeginWhile`] region, an include ([`Rule::include`] and friends), or a
24//!   list ([`Rule::patterns`]). [`Capture`] says what to do with a capture
25//!   group.
26//! - [`Error`] — what validation found, and where, as a JSON Pointer into
27//!   the output.
28//! - [`validate_regex`] and [`escape_regex`] — the regular-expression check
29//!   on its own, and literal text made safe to match.
30//!
31//! ## Example
32//!
33//! ```
34//! use textmate_lang::{BeginEnd, Grammar, Match, Rule, escape_regex};
35//!
36//! let operators = ["+", "-", "*", "/", "=="].map(escape_regex).join("|");
37//! let json = Grammar::new("source.calc")
38//!     .name("Calc")
39//!     .file_type("calc")
40//!     .pattern(Rule::include("comment"))
41//!     .pattern(Match::new(r"\b\d+(\.\d+)?\b").scope("constant.numeric.calc"))
42//!     .pattern(Match::new(operators).scope("keyword.operator.calc"))
43//!     .pattern(
44//!         Match::new(r"\b(let)\s+([a-z_]\w*)")
45//!             .capture(1, "storage.type.calc")
46//!             .capture(2, "variable.other.calc"),
47//!     )
48//!     .repository(
49//!         "comment",
50//!         BeginEnd::new("#", "$")
51//!             .scope("comment.line.number-sign.calc")
52//!             .begin_capture(0, "punctuation.definition.comment.calc"),
53//!     )
54//!     .to_json()?;
55//!
56//! assert!(json.contains(r#""match": "\\+|\\-|\\*|\\/|\\=\\=""#));
57//! // std::fs::write("syntaxes/calc.tmLanguage.json", json)?;
58//! # Ok::<(), textmate_lang::Error>(())
59//! ```
60//!
61//! ## What is checked
62//!
63//! - **Regular expressions** (`match`, `begin`, `end`, `while`,
64//!   `firstLineMatch`) are non-empty and in the subset `docs/API.md`
65//!   documents: balanced groups and classes, known escapes, quantifiers with
66//!   one meaning in every engine, fixed-length look-behind, back-references
67//!   to groups that exist (in `end` and `while`, to groups of `begin`), and
68//!   no mixing of named and numbered groups.
69//! - **Scope names** are dot-separated segments of ASCII letters, digits,
70//!   `_`, `+`, and `-`; rule names may list several scopes and substitute
71//!   capture text with `$n`, which must name an existing group.
72//! - **Captures** name groups the pattern has, each at most once.
73//! - **Includes** of repository entries resolve; includes that form a cycle
74//!   of pattern lists, which tokenizers expand forever, are refused, while
75//!   recursion through a region (how nested constructs are written) is
76//!   accepted.
77//! - **Selectors** (`injectionSelector`, injection keys) parse.
78//! - Rules nest at most [`MAX_RULE_DEPTH`] deep and patterns at most
79//!   [`MAX_REGEX_DEPTH`]; validation and emission are iterative.
80//!
81//! What cannot be checked here: rules in other grammars that external
82//! includes name, and whether a pattern matches what its author intended.
83//!
84//! ## Features
85//!
86//! - `std` (default) — the standard library. Without it the crate is `no_std`
87//!   and needs only `alloc`. There are no dependencies.
88
89#![cfg_attr(not(feature = "std"), no_std)]
90#![cfg_attr(docsrs, feature(doc_cfg))]
91#![forbid(unsafe_code)]
92#![deny(
93    warnings,
94    missing_docs,
95    unsafe_op_in_unsafe_fn,
96    unused_must_use,
97    unused_results,
98    clippy::unwrap_used,
99    clippy::expect_used,
100    clippy::panic,
101    clippy::todo,
102    clippy::unimplemented,
103    clippy::unreachable,
104    clippy::dbg_macro,
105    clippy::print_stdout,
106    clippy::print_stderr,
107    clippy::undocumented_unsafe_blocks
108)]
109
110extern crate alloc;
111
112mod check;
113mod error;
114mod grammar;
115mod json;
116mod regex;
117mod rule;
118mod scope;
119
120pub use check::MAX_RULE_DEPTH;
121pub use error::Error;
122pub use grammar::Grammar;
123pub use json::Style;
124pub use regex::{MAX_REGEX_DEPTH, RegexError, RegexErrorKind, escape_regex, validate_regex};
125pub use rule::{BeginEnd, BeginWhile, Capture, Match, Rule};
126
127/// Compiles and runs the `rust` code blocks in `README.md` and `docs/API.md` as
128/// part of `cargo test`, so the published examples cannot drift from the API.
129///
130/// Present only while collecting doctests (`#[cfg(doctest)]`); it is not part of
131/// the public surface and does not appear in the built library or its docs.
132#[cfg(doctest)]
133#[doc = include_str!("../README.md")]
134#[doc = include_str!("../docs/API.md")]
135pub struct MarkdownDocTests;