naga_oil/compose/
comment_strip_iter.rs

1use std::{borrow::Cow, str::Lines};
2
3use regex::Regex;
4
5// outside of blocks and quotes, change state on //, /* or "
6static RE_NONE: once_cell::sync::Lazy<Regex> =
7    once_cell::sync::Lazy::new(|| Regex::new(r#"(//|/\*|\")"#).unwrap());
8// in blocks, change on /* and */
9static RE_BLOCK: once_cell::sync::Lazy<Regex> =
10    once_cell::sync::Lazy::new(|| Regex::new(r"(/\*|\*/)").unwrap());
11// in quotes, change only on "
12static RE_QUOTE: once_cell::sync::Lazy<Regex> =
13    once_cell::sync::Lazy::new(|| Regex::new(r#"\""#).unwrap());
14
15#[derive(PartialEq, Eq)]
16enum CommentState {
17    None,
18    Block(usize),
19    Quote,
20}
21
22pub struct CommentReplaceIter<'a> {
23    lines: &'a mut Lines<'a>,
24    state: CommentState,
25}
26
27impl<'a> Iterator for CommentReplaceIter<'a> {
28    type Item = Cow<'a, str>;
29
30    fn next(&mut self) -> Option<Self::Item> {
31        let line_in = self.lines.next()?;
32
33        // fast path
34        if self.state == CommentState::None && !RE_NONE.is_match(line_in) {
35            return Some(Cow::Borrowed(line_in));
36        }
37
38        let mut output = String::new();
39        let mut section_start = 0;
40
41        loop {
42            let marker = match self.state {
43                CommentState::None => &RE_NONE,
44                CommentState::Block(_) => &RE_BLOCK,
45                CommentState::Quote => &RE_QUOTE,
46            }
47            .find(&line_in[section_start..]);
48
49            let section_end = marker
50                .map(|m| section_start + m.start())
51                .unwrap_or(line_in.len());
52
53            if let CommentState::Block(_) = self.state {
54                output.extend(std::iter::repeat(' ').take(section_end - section_start));
55            } else {
56                output.push_str(&line_in[section_start..section_end]);
57            }
58
59            match marker {
60                None => return Some(Cow::Owned(output)),
61                Some(marker) => {
62                    match marker.as_str() {
63                        // only possible in None state
64                        "//" => {
65                            output.extend(
66                                std::iter::repeat(' ')
67                                    .take(line_in.len() - marker.start() - section_start),
68                            );
69                            return Some(Cow::Owned(output));
70                        }
71                        // only possible in None or Block state
72                        "/*" => {
73                            self.state = match self.state {
74                                CommentState::None => CommentState::Block(1),
75                                CommentState::Block(n) => CommentState::Block(n + 1),
76                                _ => unreachable!(),
77                            };
78                            output.push_str("  ");
79                        }
80                        // only possible in Block state
81                        "*/" => {
82                            self.state = match self.state {
83                                CommentState::Block(1) => CommentState::None,
84                                CommentState::Block(n) => CommentState::Block(n - 1),
85                                _ => unreachable!(),
86                            };
87                            output.push_str("  ");
88                        }
89                        // only possible in None or Quote state
90                        "\"" => {
91                            self.state = match self.state {
92                                CommentState::None => CommentState::Quote,
93                                CommentState::Quote => CommentState::None,
94                                _ => unreachable!(),
95                            };
96                            output.push('"');
97                        }
98                        _ => unreachable!(),
99                    }
100                    section_start += marker.end();
101                }
102            }
103        }
104    }
105}
106
107pub trait CommentReplaceExt<'a> {
108    /// replace WGSL and GLSL comments with whitespace characters
109    fn replace_comments(&'a mut self) -> CommentReplaceIter<'a>;
110}
111
112impl<'a> CommentReplaceExt<'a> for Lines<'a> {
113    fn replace_comments(&'a mut self) -> CommentReplaceIter<'a> {
114        CommentReplaceIter {
115            lines: self,
116            state: CommentState::None,
117        }
118    }
119}
120
121#[test]
122fn comment_test() {
123    const INPUT: &str = r"
124not commented
125// line commented
126not commented
127/* block commented on a line */
128not commented
129// line comment with a /* block comment unterminated
130not commented
131/* block comment
132   spanning lines */
133not commented
134/* block comment
135   spanning lines and with // line comments
136   even with a // line commented terminator */
137not commented
138";
139
140    assert_eq!(
141        INPUT
142            .lines()
143            .replace_comments()
144            .zip(INPUT.lines())
145            .find(|(line, original)| {
146                (line != "not commented" && !line.chars().all(|c| c == ' '))
147                    || line.len() != original.len()
148            }),
149        None
150    );
151
152    const PARTIAL_TESTS: [(&str, &str); 11] = [
153        (
154            "1.0 /* block comment with a partial line comment on the end *// 2.0",
155            "1.0                                                           / 2.0",
156        ),
157        (
158            "1.0 /* block comment with a partial block comment on the end */* 2.0",
159            "1.0                                                            * 2.0",
160        ),
161        (
162            "1.0 /* block comment 1 *//* block comment 2 */ * 2.0",
163            "1.0                                            * 2.0",
164        ),
165        (
166            "1.0 /* block comment with real line comment after */// line comment",
167            "1.0                                                                ",
168        ),
169        ("*/", "*/"),
170        (
171            r#"#import "embedded://file.wgsl""#,
172            r#"#import "embedded://file.wgsl""#,
173        ),
174        (
175            r#"// #import "embedded://file.wgsl""#,
176            r#"                                 "#,
177        ),
178        (
179            r#"/* #import "embedded://file.wgsl" */"#,
180            r#"                                    "#,
181        ),
182        (
183            r#"/* #import "embedded:*/file.wgsl" */"#,
184            r#"                       file.wgsl" */"#,
185        ),
186        (
187            r#"#import "embedded://file.wgsl" // comment"#,
188            r#"#import "embedded://file.wgsl"           "#,
189        ),
190        (
191            r#"#import "embedded:/* */ /* /**/* / / /// * / //*/*/ / */*file.wgsl""#,
192            r#"#import "embedded:/* */ /* /**/* / / /// * / //*/*/ / */*file.wgsl""#,
193        ),
194    ];
195
196    for &(input, expected) in PARTIAL_TESTS.iter() {
197        let mut nasty_processed = input.lines();
198        let nasty_processed = nasty_processed.replace_comments().next().unwrap();
199        assert_eq!(&nasty_processed, expected);
200    }
201}
202
203#[test]
204fn multiline_comment_test() {
205    let test_cases = [
206        (
207            // Basic test
208            r"/*
209hoho
210*/",
211            r"  
212    
213  ",
214        ),
215        (
216            // Testing the commenting-out of multiline comments
217            r"///*
218hehe
219//*/",
220            r"    
221hehe
222    ",
223        ),
224        (
225            // Testing the commenting-out of single-line comments
226            r"/* // */ code goes here /*
227Still a comment // */
228/* dummy */",
229            r"         code goes here   
230                     
231           ",
232        ),
233        (
234            // A comment with a nested multiline comment
235            // Notice how the "//" inside the multiline comment doesn't take effect
236            r"/*
237//*
238*/commented
239*/not commented",
240            r"  
241   
242           
243  not commented",
244        ),
245    ];
246
247    for &(input, expected) in test_cases.iter() {
248        for (output_line, expected_line) in input.lines().replace_comments().zip(expected.lines()) {
249            assert_eq!(output_line.as_ref(), expected_line);
250        }
251    }
252}
253
254#[test]
255fn test_comment_becomes_spaces() {
256    let test_cases = [("let a/**/b =3u;", "let a    b =3u;")];
257    for &(input, expected) in test_cases.iter() {
258        for (output_line, expected_line) in input.lines().replace_comments().zip(expected.lines()) {
259            assert_eq!(output_line.as_ref(), expected_line);
260        }
261    }
262}