#![allow(dead_code)]
use super::{MinifyError, MinifyOutput, MinifyWarning};
#[derive(Debug, Clone)]
pub enum TokenKind<'a> {
Word(&'a str),
Punct(&'a str),
StrLit(&'a str),
LineComment(&'a str),
BlockComment(&'a str),
Template(&'a str),
Regex(&'a str),
Preproc(&'a str),
Newline,
}
#[derive(Debug, Clone)]
pub struct Token<'a> {
pub kind: TokenKind<'a>,
}
impl<'a> Token<'a> {
pub fn new(kind: TokenKind<'a>) -> Self {
Token { kind }
}
}
pub fn is_word_char(c: char) -> bool {
c.is_alphanumeric() || c == '_' || c == '$'
}
pub fn needs_space(prev: char, next: char) -> bool {
if is_word_char(prev) && is_word_char(next) {
return true;
}
matches!(
(prev, next),
('+', '+')
| ('-', '-')
| ('<', '<')
| ('>', '>')
| ('*', '*')
| ('/', '/')
| ('/', '*')
| ('*', '/')
| (':', ':')
| ('&', '&')
| ('|', '|')
| ('=', '=')
| ('!', '=')
| ('<', '=')
| ('>', '=')
| ('+', '=')
| ('-', '=')
| ('*', '=')
| ('/', '=')
| ('%', '=')
| ('&', '=')
| ('|', '=')
| ('^', '=')
| ('-', '>')
| ('=', '>')
| ('?', '?')
| ('?', '.')
| ('.', '.')
)
}
fn last_char(s: &str) -> Option<char> {
s.chars().next_back()
}
fn first_char(s: &str) -> Option<char> {
s.chars().next()
}
pub fn emit_aggressive(
tokens: &[Token<'_>],
opts_keep_comments: bool,
) -> Result<MinifyOutput, MinifyError> {
let mut out = String::new();
let mut warnings: Vec<MinifyWarning> = Vec::new();
let mut prev_emit_last: Option<char> = None;
for tok in tokens {
match &tok.kind {
TokenKind::Newline => {}
TokenKind::LineComment(body) => {
if !opts_keep_comments {
continue;
}
let block = format!("/*{}*/", body);
push_with_space(&mut out, &mut prev_emit_last, &block);
warnings.push(MinifyWarning::LineCommentConverted);
}
TokenKind::BlockComment(body) => {
if !opts_keep_comments {
continue;
}
let block = format!("/*{}*/", body);
push_with_space(&mut out, &mut prev_emit_last, &block);
}
TokenKind::Word(s)
| TokenKind::Punct(s)
| TokenKind::StrLit(s)
| TokenKind::Template(s)
| TokenKind::Regex(s) => {
push_with_space(&mut out, &mut prev_emit_last, s);
}
TokenKind::Preproc(s) => {
if !out.is_empty() && !out.ends_with('\n') {
out.push('\n');
}
out.push_str(s);
if !s.ends_with('\n') {
out.push('\n');
}
prev_emit_last = None;
}
}
}
Ok(MinifyOutput {
body: out,
warnings,
})
}
pub fn emit_conservative(
tokens: &[Token<'_>],
opts_keep_comments: bool,
) -> Result<MinifyOutput, MinifyError> {
let mut out = String::new();
let mut warnings: Vec<MinifyWarning> = Vec::new();
let mut prev_emit_last: Option<char> = None;
for tok in tokens {
match &tok.kind {
TokenKind::Newline => {
if !out.ends_with('\n') {
out.push('\n');
}
prev_emit_last = None;
}
TokenKind::LineComment(body) => {
if !opts_keep_comments {
continue;
}
let block = format!("/*{}*/", body);
push_with_space(&mut out, &mut prev_emit_last, &block);
warnings.push(MinifyWarning::LineCommentConverted);
}
TokenKind::BlockComment(body) => {
if !opts_keep_comments {
continue;
}
let block = format!("/*{}*/", body);
push_with_space(&mut out, &mut prev_emit_last, &block);
}
TokenKind::Word(s)
| TokenKind::Punct(s)
| TokenKind::StrLit(s)
| TokenKind::Template(s)
| TokenKind::Regex(s) => {
push_with_space(&mut out, &mut prev_emit_last, s);
}
TokenKind::Preproc(s) => {
if !out.is_empty() && !out.ends_with('\n') {
out.push('\n');
}
out.push_str(s);
if !s.ends_with('\n') {
out.push('\n');
}
prev_emit_last = None;
}
}
}
Ok(MinifyOutput {
body: out,
warnings,
})
}
fn push_with_space(out: &mut String, prev_emit_last: &mut Option<char>, s: &str) {
if s.is_empty() {
return;
}
if let Some(prev) = *prev_emit_last {
if let Some(next) = first_char(s) {
if needs_space(prev, next) {
out.push(' ');
}
}
}
out.push_str(s);
*prev_emit_last = last_char(s);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn word_word_needs_space() {
assert!(needs_space('a', 'b'));
assert!(needs_space('1', 'x'));
assert!(needs_space('_', 'a'));
}
#[test]
fn word_punct_no_space() {
assert!(!needs_space('a', '('));
assert!(!needs_space('1', ';'));
assert!(!needs_space(')', '{'));
}
#[test]
fn dangerous_pairs_need_space() {
assert!(needs_space('+', '+'));
assert!(needs_space('-', '-'));
assert!(needs_space('/', '/'));
assert!(needs_space('/', '*'));
assert!(needs_space('=', '='));
assert!(needs_space('!', '='));
assert!(needs_space('<', '='));
assert!(needs_space(':', ':'));
assert!(needs_space('&', '&'));
assert!(needs_space('|', '|'));
assert!(needs_space('-', '>'));
assert!(needs_space('=', '>'));
assert!(needs_space('.', '.'));
}
#[test]
fn safe_punct_pairs_no_space() {
assert!(!needs_space('(', '{'));
assert!(!needs_space(',', ' '));
assert!(!needs_space(';', '}'));
assert!(!needs_space(')', ';'));
assert!(!needs_space('+', 'a'));
assert!(!needs_space('a', ')'));
}
}