#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Lang {
Js,
Css,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Mode {
Code {
tpl_brace: Option<usize>,
},
Tpl,
}
pub fn strip(src: &str, lang: Lang) -> String {
let c: Vec<char> = src.chars().collect();
let mut out = String::with_capacity(src.len());
let mut mode = vec![Mode::Code { tpl_brace: None }];
let mut fresh = true;
let mut i = 0;
while i < c.len() {
let ch = c[i];
if *mode.last().unwrap() == Mode::Tpl {
if ch == '\\' && i + 1 < c.len() {
out.push(ch);
out.push(c[i + 1]);
i += 2;
continue;
}
if ch == '`' {
out.push(ch);
mode.pop();
fresh = false;
i += 1;
continue;
}
if ch == '$' && i + 1 < c.len() && c[i + 1] == '{' {
out.push_str("${");
mode.push(Mode::Code { tpl_brace: Some(0) });
i += 2;
continue;
}
out.push(ch);
i += 1;
continue;
}
if fresh && (ch == ' ' || ch == '\t') {
i += 1;
continue;
}
if ch == '\n' {
if fresh {
i += 1;
continue;
}
while out.ends_with(' ') || out.ends_with('\t') {
out.pop();
}
out.push('\n');
fresh = true;
i += 1;
continue;
}
if ch == '\r' {
i += 1;
continue;
}
if lang == Lang::Js && ch == '/' && i + 1 < c.len() && c[i + 1] == '/' {
while i < c.len() && c[i] != '\n' {
i += 1;
}
continue;
}
if ch == '/' && i + 1 < c.len() && c[i + 1] == '*' {
i += 2;
while i + 1 < c.len() && !(c[i] == '*' && c[i + 1] == '/') {
i += 1;
}
i = (i + 2).min(c.len());
continue;
}
if ch == '\'' || ch == '"' {
let quote = ch;
out.push(ch);
i += 1;
while i < c.len() {
if c[i] == '\\' && i + 1 < c.len() {
out.push(c[i]);
out.push(c[i + 1]);
i += 2;
continue;
}
out.push(c[i]);
i += 1;
if c[i - 1] == quote || c[i - 1] == '\n' {
break;
}
}
fresh = false;
continue;
}
if lang == Lang::Js && ch == '`' {
out.push(ch);
mode.push(Mode::Tpl);
fresh = false;
i += 1;
continue;
}
if lang == Lang::Js && ch == '/' && regex_here(&out) {
i = copy_regex(&c, i, &mut out);
fresh = false;
continue;
}
if lang == Lang::Js && (ch == '{' || ch == '}') {
if let Mode::Code { tpl_brace: Some(d) } = *mode.last().unwrap() {
if ch == '{' {
*mode.last_mut().unwrap() = Mode::Code {
tpl_brace: Some(d + 1),
};
} else if d == 0 {
out.push(ch);
mode.pop();
fresh = false;
i += 1;
continue;
} else {
*mode.last_mut().unwrap() = Mode::Code {
tpl_brace: Some(d - 1),
};
}
}
}
out.push(ch);
fresh = false;
i += 1;
}
while out.ends_with('\n') || out.ends_with(' ') {
out.pop();
}
out.push('\n');
out
}
fn regex_here(out: &str) -> bool {
let Some(prev) = out.chars().rev().find(|c| !c.is_whitespace()) else {
return true;
};
if "(,=:[!&|?{};+-*%<>~^".contains(prev) {
return true;
}
if prev.is_alphanumeric() || prev == '_' || prev == '$' {
let word: String = out
.chars()
.rev()
.take_while(|c| c.is_alphanumeric() || *c == '_' || *c == '$')
.collect::<Vec<_>>()
.into_iter()
.rev()
.collect();
return matches!(
word.as_str(),
"return"
| "typeof"
| "instanceof"
| "in"
| "of"
| "new"
| "delete"
| "void"
| "case"
| "do"
| "else"
| "yield"
| "await"
);
}
false
}
fn copy_regex(c: &[char], start: usize, out: &mut String) -> usize {
let mut i = start + 1;
let mut class = false;
while i < c.len() {
match c[i] {
'\n' => break,
'\\' if i + 1 < c.len() => {
i += 2;
continue;
}
'[' => class = true,
']' => class = false,
'/' if !class => {
i += 1;
while i < c.len() && c[i].is_ascii_alphabetic() {
i += 1;
}
out.extend(&c[start..i]);
return i;
}
_ => {}
}
i += 1;
}
out.push('/');
start + 1
}
#[cfg(test)]
mod tests {
use super::{strip, Lang};
fn js(s: &str) -> String {
strip(s, Lang::Js)
}
#[test]
fn comments_go_and_code_stays() {
assert_eq!(js("// gone\nlet a = 1;\n"), "let a = 1;\n");
assert_eq!(js("/* gone */let a = 1;\n"), "let a = 1;\n");
assert_eq!(js("let a = 1; // gone\n"), "let a = 1;\n");
assert_eq!(js("/** doc\n * lines\n */\nf();\n"), "f();\n");
}
#[test]
fn indentation_and_blank_lines_go() {
assert_eq!(js(" a();\n\n\n b();\n"), "a();\nb();\n");
}
#[test]
fn a_url_is_not_a_comment() {
assert_eq!(
js("const u = \"https://example.com/x\"; // gone\n"),
"const u = \"https://example.com/x\";\n"
);
assert_eq!(
js("const s = '/* not a comment */';\n"),
"const s = '/* not a comment */';\n"
);
}
#[test]
fn a_template_keeps_its_whitespace() {
let src = "const t = `line\n indented\n\n after a blank`;\n";
assert_eq!(js(src), src);
}
#[test]
fn a_template_hole_is_code_again() {
assert_eq!(
js("const t = `a${ /* gone */ b }c`;\n"),
"const t = `a${ b }c`;\n"
);
assert_eq!(
js("const t = `${ f({ x: 1 }) }`; // gone\n"),
"const t = `${ f({ x: 1 }) }`;\n"
);
assert_eq!(
js("const t = `${ `in ner` }`;\n"),
"const t = `${ `in ner` }`;\n"
);
}
#[test]
fn a_regex_survives() {
assert_eq!(
js("const r = /^\\s*\\/\\/.*$/;\n"),
"const r = /^\\s*\\/\\/.*$/;\n"
);
assert_eq!(
js("if (/[/*]/.test(s)) f();\n"),
"if (/[/*]/.test(s)) f();\n"
);
assert_eq!(js("return /a\\/b/g;\n"), "return /a\\/b/g;\n");
assert_eq!(
js("const n = a / b; // gone\nc();\n"),
"const n = a / b;\nc();\n"
);
assert_eq!(js("const n = (x) / 2 / 3;\n"), "const n = (x) / 2 / 3;\n");
}
#[test]
fn every_statement_keeps_its_line() {
let out = js("const a = 1\n// gone\nconst b = 2\n");
assert_eq!(out, "const a = 1\nconst b = 2\n");
}
#[test]
fn css_keeps_its_slashes() {
let src = ".a { background: url(//host/x.png); }\n";
assert_eq!(strip(src, Lang::Css), src);
assert_eq!(
strip("/* gone */\n.a {\n color: red;\n}\n", Lang::Css),
".a {\ncolor: red;\n}\n"
);
assert_eq!(
strip(".a::after { content: \"/* kept */\"; }\n", Lang::Css),
".a::after { content: \"/* kept */\"; }\n"
);
}
#[test]
fn stripping_twice_changes_nothing() {
for src in [
include_str!("../ui/boot.js"),
include_str!("../ui/app.js"),
include_str!("../ui/mmd.js"),
include_str!("../ui/desk.js"),
include_str!("../ui/game.js"),
include_str!("../ui/about.js"),
include_str!("../ui/find.js"),
include_str!("../ui/keys.js"),
include_str!("../ui/menu.js"),
] {
let once = js(src);
assert_eq!(js(&once), once);
}
let once = strip(include_str!("../ui/app.css"), Lang::Css);
assert_eq!(strip(&once, Lang::Css), once);
}
#[test]
fn the_real_assets_keep_their_code() {
for src in [
include_str!("../ui/app.js"),
include_str!("../ui/desk.js"),
include_str!("../ui/mmd.js"),
include_str!("../ui/boot.js"),
include_str!("../ui/game.js"),
include_str!("../ui/about.js"),
include_str!("../ui/find.js"),
include_str!("../ui/keys.js"),
include_str!("../ui/menu.js"),
] {
let out = strip(src, Lang::Js);
assert!(
subsequence(&out, src),
"the output is not the source minus parts"
);
for needle in [
"addEventListener(",
"querySelector(",
"await ",
"\"/api/",
"=> ",
] {
assert_eq!(
src.matches(needle).count(),
out.matches(needle).count(),
"{needle}: the code lost one"
);
}
assert!(out.len() * 10 < src.len() * 9, "nothing much came out");
}
}
fn subsequence(a: &str, b: &str) -> bool {
let mut it = b.chars();
a.chars().all(|c| it.any(|d| d == c))
}
}