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