badness_parser/parser/grammar/facts.rs
1//! Curated static facts about particular commands.
2//!
3//! Each set here is *closed and hand-maintained*, read as a lexical fact about
4//! the surface syntax and never as a claim about what the command does
5//! (`AGENTS.md` decision #1: meaning never enters the syntactic layer). The
6//! bodies these predicates route around are never executed, so a name that is
7//! not in a set simply degrades to the generic path.
8
9/// How [`Parser::attach_arguments`] treats a trailing `[…]` (issue #43).
10/// `[`/`]` are not real grouping in TeX, so bracket attachment is a heuristic;
11/// the policy is the caller's shape knowledge about the construct being
12/// attached to. The in-math gates apply on top of it — see
13/// [`Parser::attach_arguments`].
14#[derive(Clone, Copy, PartialEq, Eq)]
15pub(super) enum BracketPolicy {
16 /// Attach across intervening trivia (decision #8's default).
17 Greedy,
18 /// Attach only a directly-abutting `[` (a curated math environment's
19 /// `\begin`: its math body starts right after, so a detached `[` is
20 /// content).
21 Tight,
22 /// Never attach one (the delimiter-size commands: their `[` is the
23 /// delimiter being sized).
24 Forbid,
25}
26
27/// The delimiter-size commands (`\big`…`\Bigg` and their `l`/`m`/`r` variants).
28/// A closed, curated set of TeX/amsmath primitives whose sole "argument" is the
29/// delimiter token that follows (`\Big[`, `\bigl(`, `\Bigg|`), so a `[…]` after
30/// one is never an optional argument (issue #43). The static-fact posture
31/// mirrors `\left`/`\right` (`AGENTS.md`, decision #1).
32pub(super) fn is_big_delimiter_command(text: &str) -> bool {
33 let Some(name) = text.strip_prefix('\\') else {
34 return false;
35 };
36 ["bigg", "Bigg", "big", "Big"].iter().any(|s| {
37 name.strip_prefix(s)
38 .is_some_and(|rest| matches!(rest, "" | "l" | "m" | "r"))
39 })
40}
41
42/// The *definition-body* commands: commands whose trailing brace groups are
43/// macro-code bodies, where TeX does not require `\begin`/`\end` to balance
44/// within an individual group. Three families:
45///
46/// - The environment-definition commands (the LaTeX2e `\newenvironment` family
47/// and the xparse `\NewDocumentEnvironment` family): the `\begin` lives in
48/// the begin-code and its matching `\end` in the end-code by design
49/// (`\newenvironment{wrap}{\begin{center}}{\end{center}}`, issue #45).
50/// - The command-definition commands (the LaTeX2e `\newcommand` family and the
51/// xparse `\NewDocumentCommand` family): a body may open or close an
52/// environment for a matching hook to balance
53/// (`\newcommand{\@@newpage}{\end{page}\begin{page}}`, issue #55).
54/// - The LaTeX2e document/package hooks (`\AtBeginDocument` family): the code
55/// argument runs at a different point in the document, so it balances
56/// against that context, not within its own group
57/// (`\AtBeginDocument{\begin{page}}` … `\AtEndDocument{\end{page}}`).
58///
59/// Inside those bodies `\begin`/`\end` parse as plain commands (see
60/// [`Parser::in_def_body`]). A closed, curated set read as a static fact — the
61/// bodies are never executed, mirroring [`is_big_delimiter_command`].
62pub(super) fn is_definition_body_command(text: &str) -> bool {
63 matches!(
64 text,
65 "\\newenvironment"
66 | "\\renewenvironment"
67 | "\\provideenvironment"
68 | "\\NewDocumentEnvironment"
69 | "\\RenewDocumentEnvironment"
70 | "\\ProvideDocumentEnvironment"
71 | "\\DeclareDocumentEnvironment"
72 | "\\newcommand"
73 | "\\renewcommand"
74 | "\\providecommand"
75 | "\\DeclareRobustCommand"
76 | "\\NewDocumentCommand"
77 | "\\RenewDocumentCommand"
78 | "\\ProvideDocumentCommand"
79 | "\\DeclareDocumentCommand"
80 | "\\AtBeginDocument"
81 | "\\AtEndDocument"
82 | "\\AtEndOfClass"
83 | "\\AtEndOfPackage"
84 | "\\AddToHook"
85 )
86}
87
88/// The TeX `\def`-family primitives, whose next token is always the control
89/// sequence being (re)defined. A control-*symbol* name would otherwise be
90/// misparsed as live syntax — `\def\[{…}`/`\def\]{…}` (a document class
91/// restyling display math, stacks-project issue #65) reads as a math opener,
92/// `\def\\{…}` as a line break — so [`Parser::command`] consumes it as a plain
93/// token inside the `\def`'s node. A control-*word* name already parses
94/// benignly as a generic command and keeps its current shape. A closed,
95/// curated set read as a static fact, mirroring [`is_definition_body_command`];
96/// the definition is never executed.
97///
98/// Also read by the formatter's expl3 region gate (in the `badness-formatter` crate): a toggle
99/// spelling immediately preceded by one of these is a *definee*, never an executed
100/// catcode switch, so it must not open a formatter-owned region.
101pub fn is_def_prefix_command(text: &str) -> bool {
102 matches!(text, "\\def" | "\\gdef" | "\\edef" | "\\xdef")
103}