use vize_l0::{Allocator, Vec};
pub(crate) struct Logical<'a> {
pub(crate) text: &'a str,
origin: Vec<'a, u32>,
source_len: u32,
}
impl<'a> Logical<'a> {
pub(crate) fn new(allocator: &'a Allocator, source: &'a str) -> Self {
let min_indent = min_indent(source);
let mut text = vize_l0::StringBuilder::with_capacity_in(source.len(), allocator);
let mut origin: Vec<'a, u32> = Vec::with_capacity_in(source.len(), &allocator);
let mut line_start = 0usize;
for line in source.split('\n') {
if line_start != 0 {
push(&mut text, &mut origin, "\n", line_start - 1);
}
let skip = if min_indent == 0 {
0
} else {
skip_units(line, min_indent)
};
let mut body_start = line_start + skip;
let line_end = line_start + line.len();
if line_start == 0
&& crate::slice::range(source, body_start, line_end).starts_with('\u{feff}')
{
body_start += '\u{feff}'.len_utf8();
}
let joined = line_end < source.len();
let body = crate::slice::range(source, body_start, line_end);
push_normalized(&mut text, &mut origin, body, body_start, joined);
line_start = line_end + 1;
}
Self {
text: text.into_str(),
origin,
source_len: source.len() as u32,
}
}
pub(crate) fn at(&self, at: usize) -> u32 {
self.origin.get(at).copied().unwrap_or(self.source_len)
}
pub(crate) fn range(&self, start: usize, end: usize) -> (u32, u32) {
let from = self.at(start);
if end <= start {
return (from, from);
}
let last = self.origin.get(end - 1).copied().unwrap_or(self.source_len);
(from, (last + 1).min(self.source_len).max(from))
}
}
fn push<'a>(
text: &mut vize_l0::StringBuilder<'a>,
origin: &mut Vec<'a, u32>,
piece: &str,
at: usize,
) {
text.push_str(piece);
for offset in 0..piece.len() {
origin.push((at + offset) as u32);
}
}
fn push_normalized<'a>(
text: &mut vize_l0::StringBuilder<'a>,
origin: &mut Vec<'a, u32>,
body: &str,
base: usize,
joined: bool,
) {
let bytes = body.as_bytes();
let mut run = 0usize;
for (index, &byte) in bytes.iter().enumerate() {
if byte != b'\r' {
continue;
}
push(
text,
origin,
crate::slice::range(body, run, index),
base + run,
);
if index + 1 < bytes.len() || !joined {
push(text, origin, "\n", base + index);
}
run = index + 1;
}
push(text, origin, crate::slice::from(body, run), base + run);
}
fn min_indent(source: &str) -> usize {
source
.split('\n')
.filter(|line| !line.chars().all(is_js_whitespace))
.map(|line| {
line.chars()
.take_while(|&ch| is_js_whitespace(ch))
.map(char::len_utf16)
.sum::<usize>()
})
.min()
.unwrap_or(usize::MAX)
}
pub(crate) fn skip_units(line: &str, units: usize) -> usize {
let mut taken = 0usize;
for (index, ch) in line.char_indices() {
if taken >= units {
return index;
}
taken += ch.len_utf16();
}
line.len()
}
pub(crate) fn is_js_whitespace(ch: char) -> bool {
matches!(
ch,
'\t' | '\n' | '\u{b}' | '\u{c}' | '\r' | ' ' | '\u{a0}' | '\u{1680}' | '\u{2000}'
..='\u{200a}'
| '\u{2028}'
| '\u{2029}'
| '\u{202f}'
| '\u{205f}'
| '\u{3000}'
| '\u{feff}'
)
}