Skip to main content

wdl_format/token/
pre.rs

1//! Tokens emitted during the formatting of particular elements.
2
3use std::rc::Rc;
4
5use wdl_ast::SyntaxKind;
6use wdl_ast::SyntaxTokenExt;
7
8use crate::Comment;
9use crate::Token;
10use crate::TokenStream;
11use crate::Trivia;
12use crate::TriviaBlankLineSpacingPolicy;
13
14/// Normalize single-line `#@ except:` directives
15fn normalize_except_directive(text: &str) -> String {
16    let Some(remainder) = text.trim_start().strip_prefix("#@") else {
17        return text.to_owned();
18    };
19
20    let Some(rules_text) = remainder.trim_start().strip_prefix("except:") else {
21        return text.to_owned();
22    };
23
24    // Split by comma, trim each rule, and collect
25    let mut rules: Vec<String> = rules_text
26        .split(',')
27        .map(|s| s.trim().to_owned())
28        .filter(|s| !s.is_empty())
29        .collect();
30
31    // Sort rules alphabetically, case-insensitive
32    rules.sort_by_key(|a| a.to_ascii_lowercase());
33
34    // Rebuild the comment
35    format!("#@ except: {}", rules.join(", "))
36}
37
38/// A token that can be written by elements.
39///
40/// These are tokens that are intended to be written directly by elements to a
41/// [`TokenStream`](super::TokenStream) consisting of [`PreToken`]s. Note that
42/// this will transformed into a [`TokenStream`](super::TokenStream) of
43/// [`PostToken`](super::PostToken)s by a
44/// [`Postprocessor`](super::Postprocessor) (authors of elements are never
45/// expected to write [`PostToken`](super::PostToken)s directly).
46#[derive(Clone, Debug, Eq, PartialEq)]
47pub enum PreToken {
48    /// A non-trivial blank line.
49    ///
50    /// This will not be ignored by the postprocessor (unlike
51    /// [`Trivia::BlankLine`] which is potentially ignored).
52    BlankLine,
53
54    /// The end of a line.
55    LineEnd,
56
57    /// The end of a word.
58    WordEnd,
59
60    /// The start of an indented block.
61    IndentStart,
62
63    /// The end of an indented block.
64    IndentEnd,
65
66    /// How to handle trivial blank lines from this point onwards.
67    LineSpacingPolicy(TriviaBlankLineSpacingPolicy),
68
69    /// Literal text.
70    Literal(Rc<String>, SyntaxKind),
71
72    /// Trivia.
73    Trivia(Trivia),
74
75    /// A temporary indent start. Used in command section formatting.
76    ///
77    /// Command sections must account for indentation from both the
78    /// WDL context and the embedded Bash context, so this is used to
79    /// add additional indentation from the Bash context.
80    TempIndentStart,
81
82    /// A temporary indent end. Used in command section formatting.
83    ///
84    /// See [`PreToken::TempIndentStart`] for more information.
85    TempIndentEnd,
86}
87
88impl std::fmt::Display for PreToken {
89    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90        match self {
91            PreToken::BlankLine => write!(f, "<BlankLine>"),
92            PreToken::LineEnd => write!(f, "<EndOfLine>"),
93            PreToken::WordEnd => write!(f, "<WordEnd>"),
94            PreToken::IndentStart => write!(f, "<IndentStart>"),
95            PreToken::IndentEnd => write!(f, "<IndentEnd>"),
96            PreToken::LineSpacingPolicy(policy) => {
97                write!(f, "<LineSpacingPolicy@{policy:?}>")
98            }
99            PreToken::Literal(value, kind) => {
100                write!(f, "<Literal-{kind:?}@{value}>",)
101            }
102            PreToken::Trivia(trivia) => match trivia {
103                Trivia::BlankLine => {
104                    write!(f, "<OptionalBlankLine>")
105                }
106                Trivia::Comment(comment) => match comment {
107                    Comment::Preceding(value) => {
108                        write!(f, "<Comment-Preceding@{value}>",)
109                    }
110                    Comment::Inline(value) => {
111                        write!(f, "<Comment-Inline@{value}>",)
112                    }
113                },
114            },
115            PreToken::TempIndentStart => write!(f, "<TempIndentStart>"),
116            PreToken::TempIndentEnd => write!(f, "<TempIndentEnd>"),
117        }
118    }
119}
120
121impl Token for PreToken {
122    /// Returns a displayable version of the token.
123    fn display<'a>(&'a self, _config: &'a crate::Config) -> impl std::fmt::Display {
124        self
125    }
126}
127
128impl TokenStream<PreToken> {
129    /// Inserts a blank line token to the stream if the stream does not already
130    /// end with a blank line. This will replace any [`Trivia::BlankLine`]
131    /// tokens with [`PreToken::BlankLine`].
132    pub fn blank_line(&mut self) {
133        self.trim_while(|t| matches!(t, PreToken::BlankLine | PreToken::Trivia(Trivia::BlankLine)));
134        self.0.push(PreToken::BlankLine);
135    }
136
137    /// Inserts an end of line token to the stream if the stream does not
138    /// already end with an end of line token.
139    ///
140    /// This will also trim any trailing [`PreToken::WordEnd`] tokens.
141    pub fn end_line(&mut self) {
142        self.trim_while(|t| matches!(t, PreToken::WordEnd | PreToken::LineEnd));
143        self.0.push(PreToken::LineEnd);
144    }
145
146    /// Inserts a word end token to the stream if the stream does not already
147    /// end with a word end token.
148    pub fn end_word(&mut self) {
149        self.trim_end(&PreToken::WordEnd);
150        self.0.push(PreToken::WordEnd);
151    }
152
153    /// Inserts an indent start token to the stream. This will also end the
154    /// current line.
155    pub fn increment_indent(&mut self) {
156        self.end_line();
157        self.0.push(PreToken::IndentStart);
158    }
159
160    /// Inserts an indent end token to the stream. This will also end the
161    /// current line.
162    pub fn decrement_indent(&mut self) {
163        self.end_line();
164        self.0.push(PreToken::IndentEnd);
165    }
166
167    /// Inserts a trivial blank lines "always allowed" context change.
168    pub fn allow_blank_lines(&mut self) {
169        self.0.push(PreToken::LineSpacingPolicy(
170            TriviaBlankLineSpacingPolicy::Always,
171        ));
172    }
173
174    /// Inserts a trivial blank lines "not allowed after comments" context
175    /// change.
176    pub fn ignore_trailing_blank_lines(&mut self) {
177        self.0.push(PreToken::LineSpacingPolicy(
178            TriviaBlankLineSpacingPolicy::RemoveTrailingBlanks,
179        ));
180    }
181
182    /// Inserts any preceding trivia into the stream.
183    ///
184    /// # Panics
185    ///
186    /// This will panic if the provided token is itself trivia, as trivia
187    /// cannot have trivia.
188    fn push_preceding_trivia(&mut self, token: &wdl_ast::Token) {
189        assert!(!token.inner().kind().is_trivia());
190        let preceding_trivia = token.inner().preceding_trivia();
191        for token in preceding_trivia {
192            match token.kind() {
193                SyntaxKind::Whitespace => {
194                    if !self.0.last().is_some_and(|t| {
195                        matches!(t, PreToken::BlankLine | PreToken::Trivia(Trivia::BlankLine))
196                    }) {
197                        self.0.push(PreToken::Trivia(Trivia::BlankLine));
198                    }
199                }
200                SyntaxKind::Comment => {
201                    let normalized = normalize_except_directive(token.text().trim_end());
202                    let comment =
203                        PreToken::Trivia(Trivia::Comment(Comment::Preceding(Rc::new(normalized))));
204                    self.0.push(comment);
205                }
206                _ => unreachable!("unexpected trivia: {:?}", token),
207            };
208        }
209    }
210
211    /// Inserts any inline trivia into the stream.
212    ///
213    /// # Panics
214    ///
215    /// This will panic if the provided token is itself trivia, as trivia
216    /// cannot have trivia.
217    fn push_inline_trivia(&mut self, token: &wdl_ast::Token) {
218        assert!(!token.inner().kind().is_trivia());
219        if let Some(token) = token.inner().inline_comment() {
220            let inline_comment = PreToken::Trivia(Trivia::Comment(Comment::Inline(Rc::new(
221                token.text().trim_end().to_owned(),
222            ))));
223            self.0.push(inline_comment);
224        }
225    }
226
227    /// Pushes an AST token into the stream.
228    ///
229    /// This will also push any preceding or inline trivia into the stream.
230    /// Any token may have preceding or inline trivia, unless that token is
231    /// itself trivia (i.e. trivia cannot have trivia).
232    ///
233    /// # Panics
234    ///
235    /// This will panic if the provided token is trivia.
236    pub fn push_ast_token(&mut self, token: &wdl_ast::Token) {
237        self.push_preceding_trivia(token);
238        self.0.push(PreToken::Literal(
239            Rc::new(token.inner().text().to_owned()),
240            token.inner().kind(),
241        ));
242        self.push_inline_trivia(token);
243    }
244
245    /// Pushes a literal string into the stream in place of an AST token.
246    ///
247    /// This will insert any trivia that would have been inserted with the AST
248    /// token.
249    ///
250    /// # Panics
251    ///
252    /// This will panic if the provided token is trivia.
253    pub fn push_literal_in_place_of_token(&mut self, token: &wdl_ast::Token, replacement: String) {
254        self.push_preceding_trivia(token);
255        self.0.push(PreToken::Literal(
256            Rc::new(replacement),
257            token.inner().kind(),
258        ));
259        self.push_inline_trivia(token);
260    }
261
262    /// Pushes a literal string into the stream.
263    ///
264    /// This will not insert any trivia.
265    pub fn push_literal(&mut self, value: String, kind: SyntaxKind) {
266        self.0.push(PreToken::Literal(Rc::new(value), kind));
267    }
268}