use crate::host::{self, with_host, JsObj, RegExpObj};
use crate::utf16::{self, U16Index};
use fancy_regex::{Captures, Regex};
use fusevm::Value;
use indexmap::IndexMap;
use rustc_hash::FxHashMap;
use std::cell::RefCell;
use std::rc::Rc;
const SURROGATE_LO: u32 = 0xD800;
const SURROGATE_HI: u32 = 0xDFFF;
const SURROGATE_PUA_BASE: u32 = 0xF_0000;
fn remap_surrogate(cp: u32) -> u32 {
if (SURROGATE_LO..=SURROGATE_HI).contains(&cp) {
SURROGATE_PUA_BASE + (cp - SURROGATE_LO)
} else {
cp
}
}
fn escape_regexp_pattern(pattern: &str) -> String {
if pattern.is_empty() {
return "(?:)".to_string();
}
let mut out = String::with_capacity(pattern.len());
let mut in_class = false;
let mut chars = pattern.chars();
while let Some(c) = chars.next() {
match c {
'\\' => {
out.push(c);
if let Some(next) = chars.next() {
out.push(next);
}
}
'[' if !in_class => {
in_class = true;
out.push(c);
}
']' if in_class => {
in_class = false;
out.push(c);
}
'/' if !in_class => out.push_str("\\/"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\u{2028}' => out.push_str("\\u2028"),
'\u{2029}' => out.push_str("\\u2029"),
_ => out.push(c),
}
}
out
}
pub fn build_regexp(pattern: &str, flags: &str) -> Result<Value, String> {
let mut seen = String::new();
for c in flags.chars() {
if !"gimsuyd".contains(c) || seen.contains(c) {
return Err(format!(
"SyntaxError: Invalid flags supplied to RegExp constructor '{flags}'"
));
}
seen.push(c);
}
let global = flags.contains('g');
let ignore_case = flags.contains('i');
let multiline = flags.contains('m');
let dot_all = flags.contains('s');
let sticky = flags.contains('y');
let unicode = flags.contains('u');
let invalid = |reason: &str| {
format!("SyntaxError: Invalid regular expression: /{pattern}/{flags}: {reason}")
};
let rust_pat = translate(pattern, unicode).map_err(|r| invalid(&r))?;
let mut prefixed = String::new();
if ignore_case || multiline || dot_all {
prefixed.push_str("(?");
if ignore_case {
prefixed.push('i');
}
if multiline {
prefixed.push('m');
}
if dot_all {
prefixed.push('s');
}
prefixed.push(')');
}
prefixed.push_str(&rust_pat);
let re = compiled(&prefixed).map_err(|e| {
invalid(&e.lines().collect::<Vec<_>>().join(" "))
})?;
let obj = RegExpObj {
re,
source: escape_regexp_pattern(pattern),
flags: flags.to_string(),
global,
ignore_case,
multiline,
dot_all,
sticky,
unicode,
last_index: U16Index::ZERO,
};
Ok(with_host(|h| h.alloc(JsObj::RegExp(Box::new(obj)))))
}
fn compiled(prefixed: &str) -> Result<Rc<Regex>, String> {
thread_local! {
static CACHE: RefCell<FxHashMap<String, Rc<Regex>>> =
RefCell::new(FxHashMap::default());
}
if let Some(hit) = CACHE.with(|c| c.borrow().get(prefixed).cloned()) {
return Ok(hit);
}
let re = Rc::new(Regex::new(prefixed).map_err(|e| e.to_string())?);
CACHE.with(|c| {
c.borrow_mut().insert(prefixed.to_string(), re.clone());
});
Ok(re)
}
fn scan_groups(chars: &[char]) -> (usize, Vec<(String, usize)>) {
let mut count = 0usize;
let mut names = Vec::new();
let mut in_class = false;
let mut i = 0;
while i < chars.len() {
match chars[i] {
'\\' => i += 1,
'[' if !in_class => in_class = true,
']' if in_class => in_class = false,
'(' if !in_class => {
if chars.get(i + 1) != Some(&'?') {
count += 1;
} else if chars.get(i + 2) == Some(&'<')
&& !matches!(chars.get(i + 3), Some('=') | Some('!'))
{
count += 1;
let name: String = chars[i + 3..].iter().take_while(|c| **c != '>').collect();
names.push((name, count));
}
}
_ => {}
}
i += 1;
}
(count, names)
}
fn hex_escape(cp: u32) -> String {
format!("\\x{{{cp:X}}}")
}
fn legacy_octal(chars: &[char], i: usize) -> (u32, usize) {
let oct = |k: usize| chars.get(k).and_then(|c| c.to_digit(8));
let first = oct(i).unwrap_or(0);
let max = if first <= 3 { 3 } else { 2 };
let mut value = first;
let mut len = 1;
while len < max {
match oct(i + len) {
Some(d) => {
value = value * 8 + d;
len += 1;
}
None => break,
}
}
(value, len)
}
fn check_group(chars: &[char], i: usize) -> Result<(), &'static str> {
match chars.get(i + 2) {
Some(':') | Some('=') | Some('!') | Some('<') => return Ok(()),
_ => {}
}
let mut seen = String::new();
let mut k = i + 2;
let mut saw_dash = false;
while let Some(&c) = chars.get(k) {
match c {
'i' | 'm' | 's' => {
if seen.contains(c) {
return Err("Repeated flag in flag group");
}
seen.push(c);
}
'-' if !saw_dash => saw_dash = true,
':' if seen.is_empty() => return Err("Invalid flag group"),
':' => return Ok(()),
_ => return Err("Invalid group"),
}
k += 1;
}
Err("Invalid group")
}
fn translate(pat: &str, unicode: bool) -> Result<String, String> {
let chars: Vec<char> = pat.chars().collect();
let (group_count, group_names) = scan_groups(&chars);
let mut out = String::new();
let mut i = 0;
let mut in_class = false;
let mut class_pos = 0usize;
while i < chars.len() {
let c = chars[i];
if c != '\\' {
if !in_class && c == '[' {
in_class = true;
class_pos = 0;
out.push('[');
i += 1;
if chars.get(i) == Some(&'^') {
out.push('^');
i += 1;
}
continue;
}
if in_class {
if c == ']' && class_pos > 0 {
in_class = false;
out.push(']');
i += 1;
continue;
}
if c == '[' {
out.push_str("\\[");
class_pos += 1;
i += 1;
continue;
}
} else if c == '(' && chars.get(i + 1) == Some(&'?') {
check_group(&chars, i).map_err(str::to_string)?;
}
}
match c {
'\\' => {
class_pos += 1;
match chars.get(i + 1).copied() {
Some('u') => {
i += 2;
let cp_hex: String;
if chars.get(i) == Some(&'{') {
i += 1;
let mut hex = String::new();
while i < chars.len() && chars[i] != '}' {
hex.push(chars[i]);
i += 1;
}
i += 1; cp_hex = hex;
} else {
cp_hex = chars[i..(i + 4).min(chars.len())].iter().collect();
i += 4;
}
match u32::from_str_radix(cp_hex.trim(), 16) {
Ok(cp) => out.push_str(&hex_escape(remap_surrogate(cp))),
Err(_) => out.push_str(&format!("\\x{{{cp_hex}}}")),
}
continue;
}
Some('/') => {
out.push('/');
i += 2;
continue;
}
Some('c') => {
let control = match chars.get(i + 2) {
Some(x) if x.is_ascii_alphabetic() => Some(*x),
Some(x)
if in_class && !unicode && (x.is_ascii_digit() || *x == '_') =>
{
Some(*x)
}
_ => None,
};
match control {
Some(x) => {
out.push_str(&hex_escape(x as u32 % 32));
i += 3;
}
None if unicode => return Err("Invalid Unicode escape".into()),
None => {
out.push_str("\\\\");
i += 1;
}
}
continue;
}
Some(d) if d.is_ascii_digit() => {
let lone_zero =
d == '0' && !chars.get(i + 2).is_some_and(|n| n.is_ascii_digit());
if lone_zero {
out.push_str(&hex_escape(0));
i += 2;
continue;
}
if !in_class && d != '0' {
let digits: String = chars[i + 1..]
.iter()
.take_while(|c| c.is_ascii_digit())
.collect();
let n = digits.parse::<usize>().unwrap_or(usize::MAX);
if n <= group_count {
out.push_str(&format!("(?({n})\\{n}|)"));
i += 1 + digits.len();
continue;
}
}
if unicode {
let reason = if in_class || d == '0' {
"Invalid decimal escape"
} else {
"Invalid escape"
};
return Err(reason.into());
}
if d == '8' || d == '9' {
out.push(d);
i += 2;
} else {
let (cp, len) = legacy_octal(&chars, i + 1);
out.push_str(&hex_escape(cp));
i += 1 + len;
}
continue;
}
Some('k') if in_class || (group_names.is_empty() && !unicode) => {
if unicode {
return Err("Invalid class escape".into());
}
out.push('k');
i += 2;
continue;
}
Some('k') => {
if chars.get(i + 2) != Some(&'<') {
return Err("Invalid named reference".into());
}
let Some(close) = chars[i + 3..].iter().position(|c| *c == '>') else {
return Err("Invalid capture group name".into());
};
let name: String = chars[i + 3..i + 3 + close].iter().collect();
let Some((_, index)) = group_names.iter().find(|(n, _)| *n == name) else {
return Err("Invalid named capture referenced".into());
};
out.push_str(&format!("(?({index})\\{index}|)"));
i += 4 + close;
continue;
}
Some(other) => {
out.push('\\');
out.push(other);
i += 2;
continue;
}
None => {
out.push('\\');
i += 1;
}
}
}
_ => {
if in_class {
class_pos += 1;
}
out.push(c);
i += 1;
}
}
}
Ok(out)
}
fn canonical_flags(flags: &str) -> String {
"dgimsuvy"
.chars()
.filter(|c| flags.contains(*c))
.collect::<String>()
}
pub fn regexp_property(r: &RegExpObj, name: &str) -> Option<Value> {
Some(match name {
"source" => with_host(|h| h.new_str(r.source.clone())),
"flags" => with_host(|h| h.new_str(canonical_flags(&r.flags))),
"global" => Value::Bool(r.global),
"ignoreCase" => Value::Bool(r.ignore_case),
"multiline" => Value::Bool(r.multiline),
"dotAll" => Value::Bool(r.dot_all),
"sticky" => Value::Bool(r.sticky),
"unicode" => Value::Bool(r.unicode),
"hasIndices" => Value::Bool(r.flags.contains('d')),
"unicodeSets" => Value::Bool(r.flags.contains('v')),
"lastIndex" => Value::Float(r.last_index.get() as f64),
_ => return None,
})
}
pub fn is_regexp_method(name: &str) -> bool {
matches!(name, "test" | "exec" | "toString" | "compile")
|| matches!(
name,
"@@match" | "@@matchAll" | "@@search" | "@@split" | "@@replace"
)
}
pub fn regexp_method(recv: &Value, name: &str, args: Vec<Value>) -> Result<Value, String> {
match name {
"test" => {
let s = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
Ok(Value::Bool(regexp_test(recv, &s)))
}
"exec" => {
let s = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
regexp_exec(recv, &s)
}
"toString" => Ok(with_host(|h| {
let s = h.str_of(recv);
h.new_str(s)
})),
"compile" => Ok(recv.clone()),
"@@match" | "@@matchAll" | "@@search" | "@@split" | "@@replace" => {
let s = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
match name {
"@@match" => str_match(&s, recv),
"@@matchAll" => str_match_all(&s, recv),
"@@search" => str_search(&s, recv),
"@@split" => {
let limit = args.get(1).and_then(|v| {
let n = with_host(|h| h.to_number(v));
n.is_finite().then_some(n.max(0.0) as usize)
});
str_split_regex(&s, recv, limit)
}
_ => str_replace_regex(
&s,
recv,
&args.get(1).cloned().unwrap_or(Value::Undef),
false,
),
}
}
_ => Err(host::type_error(&format!("{name} is not a function"))),
}
}
fn regexp_snapshot(recv: &Value) -> Option<(Rc<Regex>, bool, bool, U16Index)> {
with_host(|h| match h.get(recv) {
Some(JsObj::RegExp(r)) => Some((r.re.clone(), r.global, r.sticky, r.last_index)),
_ => None,
})
}
fn has_indices(recv: &Value) -> bool {
with_host(|h| match h.get(recv) {
Some(JsObj::RegExp(r)) => r.flags.contains('d'),
_ => false,
})
}
fn set_last_index(recv: &Value, idx: U16Index) {
with_host(|h| {
if let Some(JsObj::RegExp(r)) = h.get_mut(recv) {
r.last_index = idx;
}
});
}
fn byte_of_index(s: &str, n: U16Index) -> usize {
utf16::byte_of_index(s, n)
}
fn index_of_byte(s: &str, byte: usize) -> U16Index {
utf16::index_of_byte(s, byte)
}
pub fn regexp_test(recv: &Value, s: &str) -> bool {
let Some((re, global, sticky, last)) = regexp_snapshot(recv) else {
return false;
};
let start_idx = if global || sticky {
last
} else {
U16Index::ZERO
};
if start_idx.get() > utf16::len(s) {
if global || sticky {
set_last_index(recv, U16Index::ZERO);
}
return false;
}
let start_byte = byte_of_index(s, start_idx);
match re.find_from_pos(s, start_byte) {
Ok(Some(m)) if !sticky || m.start() == start_byte => {
if global || sticky {
set_last_index(recv, index_of_byte(s, m.end()));
}
true
}
_ => {
if global || sticky {
set_last_index(recv, U16Index::ZERO);
}
false
}
}
}
pub fn regexp_exec(recv: &Value, s: &str) -> Result<Value, String> {
let Some((re, global, sticky, last)) = regexp_snapshot(recv) else {
return Ok(with_host(|h| h.null()));
};
let start_idx = if global || sticky {
last
} else {
U16Index::ZERO
};
if start_idx.get() > utf16::len(s) {
if global || sticky {
set_last_index(recv, U16Index::ZERO);
}
return Ok(with_host(|h| h.null()));
}
let start_byte = byte_of_index(s, start_idx);
let caps = re.captures_from_pos(s, start_byte).ok().flatten();
let caps = match caps {
Some(c) if !sticky || c.get(0).map(|m| m.start()) == Some(start_byte) => c,
_ => {
if global || sticky {
set_last_index(recv, U16Index::ZERO);
}
return Ok(with_host(|h| h.null()));
}
};
let whole = caps.get(0).unwrap();
if global || sticky {
set_last_index(recv, index_of_byte(s, whole.end()));
}
Ok(build_match_array(&re, &caps, s, has_indices(recv)))
}
fn build_match_array(re: &Regex, caps: &Captures, s: &str, indices: bool) -> Value {
let mut items: Vec<Value> = Vec::with_capacity(caps.len());
for i in 0..caps.len() {
items.push(match caps.get(i) {
Some(m) => with_host(|h| h.new_str(m.as_str().to_string())),
None => Value::Undef, });
}
let whole = caps.get(0).unwrap();
let arr = with_host(|h| h.new_array(items));
let index = index_of_byte(s, whole.start()).get();
with_host(|h| {
let idx = Value::Float(index as f64);
h.set_fn_prop(&arr, "index", idx);
let input = h.new_str(s.to_string());
h.set_fn_prop(&arr, "input", input);
});
let names: Vec<&str> = re.capture_names().flatten().collect();
if indices {
attach_indices(caps, s, &arr, &names);
}
if names.is_empty() {
with_host(|h| h.set_fn_prop(&arr, "groups", Value::Undef));
} else {
let mut g: IndexMap<String, Value> = IndexMap::new();
for name in names {
let v = match caps.name(name) {
Some(m) => with_host(|h| h.new_str(m.as_str().to_string())),
None => Value::Undef,
};
g.insert(name.to_string(), v);
}
with_host(|h| {
let obj = h.new_object(g);
let null = h.null();
h.set_proto(&obj, null);
h.set_fn_prop(&arr, "groups", obj);
});
}
arr
}
fn attach_indices(caps: &Captures, s: &str, arr: &Value, names: &[&str]) {
let pair = |m: Option<fancy_regex::Match>| match m {
Some(m) => {
let (a, b) = (index_of_byte(s, m.start()), index_of_byte(s, m.end()));
with_host(|h| {
h.new_array(vec![
Value::Float(a.get() as f64),
Value::Float(b.get() as f64),
])
})
}
None => Value::Undef,
};
let mut pairs: Vec<Value> = Vec::with_capacity(caps.len());
for i in 0..caps.len() {
pairs.push(pair(caps.get(i)));
}
let idx_arr = with_host(|h| h.new_array(pairs));
if names.is_empty() {
with_host(|h| h.set_fn_prop(&idx_arr, "groups", Value::Undef));
} else {
let mut g: IndexMap<String, Value> = IndexMap::new();
for name in names {
g.insert((*name).to_string(), pair(caps.name(name)));
}
with_host(|h| {
let obj = h.new_object(g);
let null = h.null();
h.set_proto(&obj, null);
h.set_fn_prop(&idx_arr, "groups", obj);
});
}
with_host(|h| h.set_fn_prop(arr, "indices", idx_arr));
}
pub fn str_match(s: &str, re_val: &Value) -> Result<Value, String> {
let Some((re, global, _, _)) = regexp_snapshot(re_val) else {
return Ok(with_host(|h| h.null()));
};
if !global {
set_last_index(re_val, U16Index::ZERO);
return regexp_exec_from_zero(&re, s, has_indices(re_val));
}
set_last_index(re_val, U16Index::ZERO);
let matches: Vec<Value> = re
.find_iter(s)
.filter_map(|m| m.ok())
.map(|m| with_host(|h| h.new_str(m.as_str().to_string())))
.collect();
if matches.is_empty() {
Ok(with_host(|h| h.null()))
} else {
Ok(with_host(|h| h.new_array(matches)))
}
}
fn regexp_exec_from_zero(re: &Regex, s: &str, indices: bool) -> Result<Value, String> {
match re.captures(s).ok().flatten() {
Some(caps) => Ok(build_match_array(re, &caps, s, indices)),
None => Ok(with_host(|h| h.null())),
}
}
pub fn str_match_all(s: &str, re_val: &Value) -> Result<Value, String> {
let Some((re, global, _, _)) = regexp_snapshot(re_val) else {
return Ok(with_host(|h| h.new_array(Vec::new())));
};
let indices = has_indices(re_val);
if !global {
return Err(host::type_error(
"String.prototype.matchAll called with a non-global RegExp argument",
));
}
let mut items = Vec::new();
for caps in re.captures_iter(s).flatten() {
items.push(build_match_array(&re, &caps, s, indices));
}
Ok(with_host(|h| {
h.alloc(JsObj::Iter {
items,
idx: 0,
array: None,
})
}))
}
pub fn str_search(s: &str, re_val: &Value) -> Result<Value, String> {
let Some((re, _, _, _)) = regexp_snapshot(re_val) else {
return Ok(Value::Float(-1.0));
};
Ok(match re.find(s).ok().flatten() {
Some(m) => Value::Float(index_of_byte(s, m.start()).get() as f64),
None => Value::Float(-1.0),
})
}
pub fn str_split_regex(s: &str, re_val: &Value, limit: Option<usize>) -> Result<Value, String> {
let Some((re, _, _, _)) = regexp_snapshot(re_val) else {
return Ok(with_host(|h| h.new_array(Vec::new())));
};
let lim = limit.unwrap_or(usize::MAX);
if lim == 0 {
return Ok(with_host(|h| h.new_array(Vec::new())));
}
let size = utf16::len(s);
let units = |i: usize| byte_of_index(s, U16Index::new(i));
if size == 0 {
let out = if matches!(re.find(s), Ok(Some(_))) {
Vec::new()
} else {
vec![with_host(|h| h.new_str(String::new()))]
};
return Ok(with_host(|h| h.new_array(out)));
}
let mut out: Vec<Value> = Vec::new();
let mut p = 0usize; let mut q = 0usize; while q < size {
let Some(caps) = re.captures_from_pos(s, units(q)).ok().flatten() else {
break;
};
let m = caps.get(0).expect("group 0 always participates");
let m_start = index_of_byte(s, m.start()).get();
if m_start >= size {
break;
}
let e = index_of_byte(s, m.end()).get().min(size);
if e == p {
q = m_start.max(q) + 1;
continue;
}
out.push(with_host(|h| {
h.new_str(utf16::Units::of(s).slice(p, m_start))
}));
if out.len() >= lim {
return Ok(with_host(|h| h.new_array(out)));
}
for i in 1..caps.len() {
out.push(match caps.get(i) {
Some(g) => with_host(|h| h.new_str(g.as_str().to_string())),
None => Value::Undef,
});
if out.len() >= lim {
return Ok(with_host(|h| h.new_array(out)));
}
}
p = e;
q = p;
}
out.push(with_host(|h| h.new_str(utf16::Units::of(s).slice(p, size))));
out.truncate(lim);
Ok(with_host(|h| h.new_array(out)))
}
pub fn str_replace_regex(
s: &str,
re_val: &Value,
repl: &Value,
all: bool,
) -> Result<Value, String> {
let Some((re, global, _, _)) = regexp_snapshot(re_val) else {
return Ok(with_host(|h| h.new_str(s.to_string())));
};
let replace_all = all || global;
let is_fn = with_host(|h| host::is_callable(h, repl));
let mut out = String::new();
let mut last = 0usize;
let mut count = 0;
for caps in re.captures_iter(s).flatten() {
let m = caps.get(0).unwrap();
out.push_str(&s[last..m.start()]);
if is_fn {
let mut call_args: Vec<Value> = Vec::new();
for i in 0..caps.len() {
call_args.push(match caps.get(i) {
Some(g) => with_host(|h| h.new_str(g.as_str().to_string())),
None => Value::Undef,
});
}
call_args.push(Value::Float(index_of_byte(s, m.start()).get() as f64));
call_args.push(with_host(|h| h.new_str(s.to_string())));
if let Some(groups) = named_groups_object(&re, &caps) {
call_args.push(groups);
}
let r = host::invoke(repl, call_args, None)?;
out.push_str(&with_host(|h| h.str_of(&r)));
} else {
let repl_str = with_host(|h| h.str_of(repl));
out.push_str(&expand_replacement(&repl_str, &caps, s));
}
last = m.end();
count += 1;
if !replace_all && count >= 1 {
break;
}
}
out.push_str(&s[last..]);
if global {
set_last_index(re_val, U16Index::ZERO);
}
Ok(with_host(|h| h.new_str(out)))
}
fn named_groups_object(re: &Regex, caps: &Captures) -> Option<Value> {
let names: Vec<&str> = re.capture_names().flatten().collect();
if names.is_empty() {
return None;
}
let mut g: IndexMap<String, Value> = IndexMap::new();
for name in names {
let v = match caps.name(name) {
Some(m) => with_host(|h| h.new_str(m.as_str().to_string())),
None => Value::Undef,
};
g.insert(name.to_string(), v);
}
Some(with_host(|h| {
let obj = h.new_object(g);
let null = h.null();
h.set_proto(&obj, null);
obj
}))
}
fn expand_replacement(templ: &str, caps: &Captures, s: &str) -> String {
let chars: Vec<char> = templ.chars().collect();
let mut out = String::new();
let mut i = 0;
let whole = caps.get(0).unwrap();
while i < chars.len() {
if chars[i] == '$' && i + 1 < chars.len() {
let n = chars[i + 1];
match n {
'$' => {
out.push('$');
i += 2;
}
'&' => {
out.push_str(whole.as_str());
i += 2;
}
'`' => {
out.push_str(&s[..whole.start()]);
i += 2;
}
'\'' => {
out.push_str(&s[whole.end()..]);
i += 2;
}
'<' => {
let mut j = i + 2;
let mut name = String::new();
while j < chars.len() && chars[j] != '>' {
name.push(chars[j]);
j += 1;
}
if let Some(m) = caps.name(&name) {
out.push_str(m.as_str());
}
i = j + 1; }
d if d.is_ascii_digit() => {
let d2 = chars.get(i + 2).copied().filter(|c| c.is_ascii_digit());
let two = d2.and_then(|c2| format!("{d}{c2}").parse::<usize>().ok());
if let Some(gi) = two.filter(|gi| *gi < caps.len()) {
if let Some(g) = caps.get(gi) {
out.push_str(g.as_str());
}
i += 3;
} else {
let gi = d.to_digit(10).unwrap() as usize;
if gi >= 1 && gi < caps.len() {
if let Some(g) = caps.get(gi) {
out.push_str(g.as_str());
}
i += 2;
} else {
out.push('$');
i += 1;
}
}
}
_ => {
out.push('$');
i += 1;
}
}
} else {
out.push(chars[i]);
i += 1;
}
}
out
}