Skip to main content

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}