1#![allow(dead_code)]
7
8use super::{MinifyError, MinifyOutput, MinifyWarning};
9
10#[derive(Debug, Clone)]
11pub enum TokenKind<'a> {
12 Word(&'a str),
15 Punct(&'a str),
19 StrLit(&'a str),
21 LineComment(&'a str),
24 BlockComment(&'a str),
26 Template(&'a str),
28 Regex(&'a str),
30 Preproc(&'a str),
33 Newline,
35}
36
37#[derive(Debug, Clone)]
38pub struct Token<'a> {
39 pub kind: TokenKind<'a>,
40}
41
42impl<'a> Token<'a> {
43 pub fn new(kind: TokenKind<'a>) -> Self {
44 Token { kind }
45 }
46}
47
48pub fn is_word_char(c: char) -> bool {
53 c.is_alphanumeric() || c == '_' || c == '$'
54}
55
56pub fn needs_space(prev: char, next: char) -> bool {
61 if is_word_char(prev) && is_word_char(next) {
62 return true;
63 }
64 matches!(
66 (prev, next),
67 ('+', '+')
68 | ('-', '-')
69 | ('<', '<')
70 | ('>', '>')
71 | ('*', '*')
72 | ('/', '/')
73 | ('/', '*')
74 | ('*', '/')
75 | (':', ':')
76 | ('&', '&')
77 | ('|', '|')
78 | ('=', '=')
79 | ('!', '=')
80 | ('<', '=')
81 | ('>', '=')
82 | ('+', '=')
83 | ('-', '=')
84 | ('*', '=')
85 | ('/', '=')
86 | ('%', '=')
87 | ('&', '=')
88 | ('|', '=')
89 | ('^', '=')
90 | ('-', '>')
91 | ('=', '>')
92 | ('?', '?')
93 | ('?', '.')
94 | ('.', '.')
95 )
96}
97
98fn last_char(s: &str) -> Option<char> {
99 s.chars().next_back()
100}
101fn first_char(s: &str) -> Option<char> {
102 s.chars().next()
103}
104
105pub fn safe_block_comment(body: &str) -> String {
109 format!("/*{}*/", body.replace("*/", "* /"))
110}
111
112pub fn emit_aggressive(
115 tokens: &[Token<'_>],
116 opts_keep_comments: bool,
117) -> Result<MinifyOutput, MinifyError> {
118 let mut out = String::new();
119 let mut warnings: Vec<MinifyWarning> = Vec::new();
120 let mut prev_emit_last: Option<char> = None;
121 for tok in tokens {
122 match &tok.kind {
123 TokenKind::Newline => {}
124 TokenKind::LineComment(body) => {
125 if !opts_keep_comments {
126 continue;
127 }
128 let block = safe_block_comment(body);
129 push_with_space(&mut out, &mut prev_emit_last, &block);
130 warnings.push(MinifyWarning::LineCommentConverted);
131 }
132 TokenKind::BlockComment(body) => {
133 if !opts_keep_comments {
134 continue;
135 }
136 let block = safe_block_comment(body);
137 push_with_space(&mut out, &mut prev_emit_last, &block);
138 }
139 TokenKind::Word(s)
140 | TokenKind::Punct(s)
141 | TokenKind::StrLit(s)
142 | TokenKind::Template(s)
143 | TokenKind::Regex(s) => {
144 push_with_space(&mut out, &mut prev_emit_last, s);
145 }
146 TokenKind::Preproc(s) => {
147 if !out.is_empty() && !out.ends_with('\n') {
148 out.push('\n');
149 }
150 out.push_str(s);
151 if !s.ends_with('\n') {
152 out.push('\n');
153 }
154 prev_emit_last = None;
155 }
156 }
157 }
158 Ok(MinifyOutput {
159 body: out,
160 warnings,
161 })
162}
163
164pub fn emit_conservative(
167 tokens: &[Token<'_>],
168 opts_keep_comments: bool,
169) -> Result<MinifyOutput, MinifyError> {
170 let mut out = String::new();
171 let mut warnings: Vec<MinifyWarning> = Vec::new();
172 let mut prev_emit_last: Option<char> = None;
173 for tok in tokens {
174 match &tok.kind {
175 TokenKind::Newline => {
176 if !out.ends_with('\n') {
179 out.push('\n');
180 }
181 prev_emit_last = None;
182 }
183 TokenKind::LineComment(body) => {
184 if !opts_keep_comments {
185 continue;
186 }
187 let block = safe_block_comment(body);
188 push_with_space(&mut out, &mut prev_emit_last, &block);
189 warnings.push(MinifyWarning::LineCommentConverted);
190 }
191 TokenKind::BlockComment(body) => {
192 if !opts_keep_comments {
193 continue;
194 }
195 let block = safe_block_comment(body);
196 push_with_space(&mut out, &mut prev_emit_last, &block);
197 }
198 TokenKind::Word(s)
199 | TokenKind::Punct(s)
200 | TokenKind::StrLit(s)
201 | TokenKind::Template(s)
202 | TokenKind::Regex(s) => {
203 push_with_space(&mut out, &mut prev_emit_last, s);
204 }
205 TokenKind::Preproc(s) => {
206 if !out.is_empty() && !out.ends_with('\n') {
208 out.push('\n');
209 }
210 out.push_str(s);
211 if !s.ends_with('\n') {
212 out.push('\n');
213 }
214 prev_emit_last = None;
215 }
216 }
217 }
218 Ok(MinifyOutput {
219 body: out,
220 warnings,
221 })
222}
223
224fn push_with_space(out: &mut String, prev_emit_last: &mut Option<char>, s: &str) {
225 if s.is_empty() {
226 return;
227 }
228 if let Some(prev) = *prev_emit_last {
229 if let Some(next) = first_char(s) {
230 if needs_space(prev, next) {
231 out.push(' ');
232 }
233 }
234 }
235 out.push_str(s);
236 *prev_emit_last = last_char(s);
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242
243 #[test]
244 fn word_word_needs_space() {
245 assert!(needs_space('a', 'b'));
246 assert!(needs_space('1', 'x'));
247 assert!(needs_space('_', 'a'));
248 }
249
250 #[test]
251 fn word_punct_no_space() {
252 assert!(!needs_space('a', '('));
253 assert!(!needs_space('1', ';'));
254 assert!(!needs_space(')', '{'));
255 }
256
257 #[test]
258 fn dangerous_pairs_need_space() {
259 assert!(needs_space('+', '+'));
260 assert!(needs_space('-', '-'));
261 assert!(needs_space('/', '/'));
262 assert!(needs_space('/', '*'));
263 assert!(needs_space('=', '='));
264 assert!(needs_space('!', '='));
265 assert!(needs_space('<', '='));
266 assert!(needs_space(':', ':'));
267 assert!(needs_space('&', '&'));
268 assert!(needs_space('|', '|'));
269 assert!(needs_space('-', '>'));
270 assert!(needs_space('=', '>'));
271 assert!(needs_space('.', '.'));
272 }
273
274 #[test]
275 fn safe_punct_pairs_no_space() {
276 assert!(!needs_space('(', '{'));
277 assert!(!needs_space(',', ' '));
278 assert!(!needs_space(';', '}'));
279 assert!(!needs_space(')', ';'));
280 assert!(!needs_space('+', 'a'));
281 assert!(!needs_space('a', ')'));
282 }
283}