badness_parser/syntax.rs
1//! `SyntaxKind` — the kinds of CST tokens and nodes — and the rowan `Language`
2//! binding for badness's LaTeX surface CST.
3
4/// Kinds of tokens (terminals, from the lexer) and nodes (composites, from the
5/// parser) in the CST.
6///
7/// Token kinds come first, node kinds after; `ROOT` is kept **last** so
8/// [`rowan::Language::kind_from_raw`] can bounds-check the raw discriminant with
9/// a single comparison. Keep the hidden end marker immediately after `ROOT`.
10#[allow(non_camel_case_types)]
11#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
12#[repr(u16)]
13pub enum SyntaxKind {
14 // --- Tokens (terminals, produced by the lexer) ---
15 CONTROL_WORD, // `\foo` (backslash + ASCII letters)
16 CONTROL_SYMBOL, // `\\`, `\{`, `\%`, `\,` … (backslash + one non-letter)
17 L_BRACE, // {
18 R_BRACE, // }
19 L_BRACKET, // [
20 R_BRACKET, // ]
21 DOLLAR, // $
22 AMPERSAND, // &
23 HASH, // #
24 CARET, // ^
25 UNDERSCORE, // _
26 TILDE, // ~
27 COMMENT, // `% …` up to (not including) the line break
28 WHITESPACE, // spaces / tabs
29 NEWLINE, // `\n`, `\r\n`, or `\r`
30 WORD, // a run of ordinary text characters
31 VERB, // `\verb|…|` / `\verb*|…|` inline verbatim (a single token)
32 VERBATIM_BODY, // the raw body of a verbatim-like environment (a single token)
33 DOC_MARGIN, // a `.dtx` documentation line's leading `%` margin (trivia)
34 GUARD, // a `.dtx` docstrip guard `%<…>` (`%<*t>`/`%</t>`/inline) (trivia)
35 ERROR, // lexer fallback; the lexer is total, so this is unused today
36
37 // Nodes
38 GROUP, // { … }
39 OPTIONAL, // [ … ] optional argument
40 ARGUMENT, // an argument attached to a command
41 COMMAND, // a control sequence with its arguments
42 ENVIRONMENT, // \begin{…} … \end{…}
43 BEGIN, // \begin{name}
44 END, // \end{name}
45 NAME_GROUP, // {name} following \begin / \end
46 // `\if…\else…\or…\fi`, when the shape gate pairs it. The construct is a run
47 // of `CONDITIONAL_BRANCH` nodes followed by the closing `\fi` as the last
48 // child, mirroring `ENVIRONMENT > BEGIN … END`. The `\if` test's extent is
49 // not statically resolvable (`\ifnum\radius>5` scans ⟨number⟩⟨rel⟩⟨number⟩
50 // by TeX's own scanner), so the opener and its test ride the *first* branch
51 // rather than a head node of their own.
52 CONDITIONAL,
53 // One branch of a `CONDITIONAL`. The first holds the opener, its test, and
54 // the then-body; every later one *starts with* its `\else`/`\or` divider, so
55 // a consumer finds the boundaries positionally and never by command name.
56 CONDITIONAL_BRANCH,
57 INLINE_MATH, // $ … $ or \( … \)
58 DISPLAY_MATH, // $$ … $$ or \[ … \]
59 MATH, // a math body (the atoms between the delimiters)
60 SCRIPTED, // a base atom with attached scripts: base (SUBSCRIPT | SUPERSCRIPT)+
61 SUBSCRIPT, // `_` and its tightly-bound script argument
62 SUPERSCRIPT, // `^` and its tightly-bound script argument
63 LEFT_RIGHT, // `\left( … \right)` — a matched delimiter pair wrapping a MATH body
64 PARAGRAPH, // text delimited by blank lines
65 DOC_COMMENT, // a bound leading-`%` comment run, grouped before its construct
66 TEXT, // a run of text and trivia
67 LINE_BREAK, // `\\`, with a tightly-bound `*` and/or `[len]` (`\\*[2ex]`)
68 // A `;`-terminated statement in a curated `statementBody` environment body
69 // (the TikZ/pgf picture family). Owns everything from the statement's first
70 // non-trivia element through the top-level WORD carrying the terminating
71 // `;`, so layout can derive statement boundaries from structure rather than
72 // authored newlines. A run that never reaches a `;` is left as plain
73 // paragraph content — recognition is retrospective and degrades silently.
74 STATEMENT,
75 ROOT, // the document root (keep LAST)
76 #[doc(hidden)]
77 __LAST,
78}
79
80impl SyntaxKind {
81 /// The number of `SyntaxKind` variants. Sound because the enum is
82 /// `#[repr(u16)]` with contiguous discriminants `0..=ROOT` and `ROOT` is kept
83 /// last; used to size kind-indexed tables (e.g. the linter's dispatch table).
84 pub const COUNT: usize = SyntaxKind::__LAST as usize;
85}
86
87/// The rowan language marker for badness's CST.
88#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
89pub enum BadnessLang {}
90
91impl_rowan_lang!(BadnessLang, SyntaxKind, "LaTeX");
92
93pub type SyntaxNode = rowan::SyntaxNode<BadnessLang>;
94pub type SyntaxToken = rowan::SyntaxToken<BadnessLang>;
95pub type SyntaxElement = rowan::SyntaxElement<BadnessLang>;
96
97/// Whitespace and newlines are the only trivia the formatter rewrites; comments
98/// are preserved verbatim and so are *not* collapsible. A shared shape
99/// predicate: the formatter's gap normalization and the semantic layer's expl3
100/// statement segmentation both skip exactly this class.
101pub fn is_collapsible_trivia(kind: SyntaxKind) -> bool {
102 matches!(kind, SyntaxKind::WHITESPACE | SyntaxKind::NEWLINE)
103}
104
105/// Trivia ignored by syntax readers: collapsible whitespace plus comments.
106pub fn is_trivia(kind: SyntaxKind) -> bool {
107 is_collapsible_trivia(kind) || kind == SyntaxKind::COMMENT
108}
109
110/// Whether a `WORD` token is a single TeX parameter digit (`1`..=`9`) — the
111/// shape that follows `#` in a parameter reference. Reads only the token text.
112pub fn is_param_digit(t: &SyntaxToken) -> bool {
113 matches!(t.text().as_bytes(), [b'1'..=b'9'])
114}