Skip to main content

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}