use oxc_allocator::Allocator;
use oxc_ast::ast::{StringLiteral, TemplateLiteral};
use oxc_parser::Parser;
use oxc_span::SourceType;
pub fn mask(source: &str) -> (String, Vec<String>) {
if let Some(result) = mask_with_oxc(source) {
return result;
}
mask_heuristic(source)
}
pub fn unmask(source: &str, masks: &[String]) -> String {
let mut result = source.to_string();
for (idx, content) in masks.iter().enumerate() {
result = result.replace(&format!("__RWE_MASK_{idx}__"), content);
}
result
}
fn mask_with_oxc(source: &str) -> Option<(String, Vec<String>)> {
let alloc = Allocator::default();
let source_type = SourceType::default()
.with_module(true)
.with_jsx(true)
.with_typescript(true);
let parsed = Parser::new(&alloc, source, source_type).parse();
if parsed.panicked {
return None;
}
let mut collector = SpanCollector { spans: Vec::new() };
oxc_ast_visit::walk::walk_program(&mut collector, &parsed.program);
collector.spans.sort_by_key(|s| s.0);
collector.spans.dedup();
let mut masks: Vec<String> = Vec::new();
let mut result = String::with_capacity(source.len());
let mut cursor: usize = 0;
for (start, end) in &collector.spans {
let s = *start as usize;
let e = *end as usize;
if s > source.len() || e > source.len() || s >= e {
continue;
}
if s > cursor {
result.push_str(&source[cursor..s]);
}
let span_text = &source[s..e];
if span_text.starts_with('`') {
result.push('`');
let inner = &span_text[1..span_text.len().saturating_sub(1)];
let idx = masks.len();
masks.push(inner.to_string());
result.push_str(&format!("__RWE_MASK_{idx}__"));
if span_text.ends_with('`') {
result.push('`');
}
} else if span_text.starts_with('"') || span_text.starts_with('\'') {
let quote = span_text.chars().next().unwrap();
result.push(quote);
let inner = &span_text[1..span_text.len().saturating_sub(1)];
let idx = masks.len();
masks.push(inner.to_string());
result.push_str(&format!("__RWE_MASK_{idx}__"));
if span_text.ends_with(quote) {
result.push(quote);
}
} else {
result.push_str(span_text);
}
cursor = e;
}
if cursor < source.len() {
result.push_str(&source[cursor..]);
}
Some((result, masks))
}
struct SpanCollector {
spans: Vec<(u32, u32)>,
}
impl<'a> oxc_ast_visit::Visit<'a> for SpanCollector {
fn visit_string_literal(&mut self, lit: &StringLiteral<'a>) {
self.spans.push((lit.span.start, lit.span.end));
}
fn visit_template_literal(&mut self, lit: &TemplateLiteral<'a>) {
self.spans.push((lit.span.start, lit.span.end));
}
}
fn mask_heuristic(source: &str) -> (String, Vec<String>) {
let mut masks: Vec<String> = Vec::new();
let mut result = String::with_capacity(source.len());
let chars: Vec<char> = source.chars().collect();
let len = chars.len();
let mut i = 0;
while i < len {
if chars[i] == '/' && i + 1 < len && chars[i + 1] == '/' {
result.push(chars[i]);
i += 1;
result.push(chars[i]);
i += 1;
while i < len && chars[i] != '\n' {
result.push(chars[i]);
i += 1;
}
if i < len {
result.push(chars[i]); }
i += 1;
continue;
}
if chars[i] == '/' && i + 1 < len && chars[i + 1] == '*' {
result.push(chars[i]);
i += 1;
result.push(chars[i]);
i += 1;
while i < len {
if chars[i] == '*' && i + 1 < len && chars[i + 1] == '/' {
result.push(chars[i]);
i += 1;
result.push(chars[i]);
i += 1;
break;
}
result.push(chars[i]);
i += 1;
}
continue;
}
if chars[i] == '/' && i + 1 < len && chars[i + 1] != '/' && chars[i + 1] != '*' {
let prev_significant = result.chars().rev().find(|c| !c.is_ascii_whitespace());
let is_regex = match prev_significant {
None => true,
Some(c) => matches!(
c,
'(' | '='
| '['
| '!'
| '&'
| '|'
| '?'
| ':'
| ','
| ';'
| '{'
| '}'
| '~'
| '^'
| '%'
| '<'
| '>'
| '+'
| '-'
| '*'
| '/'
| '\n'
),
};
if is_regex {
result.push(chars[i]);
i += 1;
while i < len {
if chars[i] == '\\' {
result.push(chars[i]);
i += 1;
if i < len {
result.push(chars[i]);
i += 1;
}
} else if chars[i] == '/' {
result.push(chars[i]);
i += 1;
while i < len && chars[i].is_ascii_alphabetic() {
result.push(chars[i]);
i += 1;
}
break;
} else if chars[i] == '\n' {
break;
} else {
result.push(chars[i]);
i += 1;
}
}
continue;
}
}
match chars[i] {
'"' | '\'' => {
let quote = chars[i];
let mut content = String::new();
i += 1;
while i < len {
let ch = chars[i];
if ch == '\\' {
content.push(ch);
i += 1;
if i < len {
content.push(chars[i]);
}
} else if ch == quote {
break;
} else if ch == '\n' {
content.push(ch);
break;
} else {
content.push(ch);
}
i += 1;
}
let idx = masks.len();
masks.push(content);
result.push(quote);
result.push_str(&format!("__RWE_MASK_{idx}__"));
result.push(quote);
}
'`' => {
let mut content = String::new();
i += 1;
let mut depth = 0i32;
while i < len {
let ch = chars[i];
if ch == '\\' {
content.push(ch);
i += 1;
if i < len {
content.push(chars[i]);
}
} else if ch == '$' && i + 1 < len && chars[i + 1] == '{' {
depth += 1;
content.push(ch);
i += 1;
content.push(chars[i]);
} else if ch == '}' {
if depth > 0 {
depth -= 1;
}
content.push(ch);
} else if ch == '`' && depth == 0 {
break;
} else {
content.push(ch);
}
i += 1;
}
let idx = masks.len();
masks.push(content);
result.push('`');
result.push_str(&format!("__RWE_MASK_{idx}__"));
result.push('`');
}
c => result.push(c),
}
i += 1;
}
(result, masks)
}