use std::ops::Range;
#[cfg(feature = "esl")]
use super::extent;
use super::Traced;
#[cfg(feature = "sdp")]
use super::{trace, untrace};
pub(crate) struct Cut {
pub(crate) spans: Vec<Range<usize>>,
#[cfg(feature = "esl")]
pub(crate) held_delimiter: bool,
#[cfg(feature = "esl")]
pub(crate) open_quote: bool,
}
pub(crate) fn skip_spaces(text: &[Traced]) -> &[Traced] {
let count = text
.iter()
.take_while(|&&(c, ..)| c == ' ')
.count();
&text[count..]
}
fn has_quote(rest: &[Traced]) -> bool {
rest.iter()
.take_while(|&&(c, ..)| c != '\0')
.any(|&(c, ..)| c == '\'')
}
pub(crate) fn char_delim(text: &[Traced], delim: char, limit: usize) -> Cut {
let mut spans = Vec::new();
let mut begin = None;
let mut inside_quotes = false;
#[cfg(feature = "esl")]
let mut held_delimiter = false;
let mut stop = text.len();
let mut i = 0;
while i < text.len() {
if text[i].0 == '\0' {
stop = i;
break;
}
let start = match begin {
Some(start) => start,
None if spans.len() + 1 >= limit => {
begin = Some(i);
break;
}
None => *begin.insert(i),
};
let c = text[i].0;
if c == '\\' {
i += 1;
} else if c == '\'' && (inside_quotes || has_quote(&text[i + 1..])) {
inside_quotes = !inside_quotes;
} else if c == delim && !inside_quotes {
spans.push(start..i);
begin = None;
} else if c == delim {
#[cfg(feature = "esl")]
{
held_delimiter = true;
}
}
i += 1;
}
if let Some(start) = begin {
spans.push(start..stop);
}
Cut {
spans,
#[cfg(feature = "esl")]
held_delimiter,
#[cfg(feature = "esl")]
open_quote: inside_quotes,
}
}
#[cfg(feature = "esl")]
pub(crate) fn blank_delim(text: &[Traced], limit: usize) -> Cut {
enum State {
Start,
SkipInitialSpace,
FindDelim,
SkipEndingSpace,
}
let mut spans = Vec::new();
let mut state = State::Start;
let mut begin = 0;
let mut inside_quotes = false;
let mut held_delimiter = false;
let mut stop = text.len();
let mut i = 0;
while i < text.len() {
let c = text[i].0;
if c == '\0' {
stop = i;
break;
}
match state {
State::Start if spans.len() + 1 >= limit => {
begin = i;
state = State::FindDelim;
break;
}
State::Start => {
begin = i;
state = State::SkipInitialSpace;
}
State::SkipInitialSpace | State::SkipEndingSpace if c == ' ' => i += 1,
State::SkipInitialSpace => state = State::FindDelim,
State::SkipEndingSpace => state = State::Start,
State::FindDelim => {
if c == '\\' {
i += 1;
} else if c == '\'' {
inside_quotes = !inside_quotes;
} else if c == ' ' && !inside_quotes {
spans.push(begin..i);
state = State::SkipEndingSpace;
} else if c == ' ' {
held_delimiter = true;
}
i += 1;
}
}
}
if matches!(state, State::SkipInitialSpace | State::FindDelim) {
spans.push(begin..stop);
}
Cut {
spans,
held_delimiter,
open_quote: inside_quotes,
}
}
fn unescape(next: char, delim: Option<char>) -> Option<char> {
if Some(next) == delim || matches!(next, '\'' | '"' | '\\') {
return Some(next);
}
match next {
'n' => Some('\n'),
'r' => Some('\r'),
't' => Some('\t'),
's' => Some(' '),
_ => None,
}
}
pub(crate) fn takes_a_backslash(c: char, delim: Option<char>) -> bool {
matches!(c, '\\' | '\'') || Some(c) == delim
}
pub(crate) fn escape_inside_token(text: &str, delim: Option<char>) -> String {
let mut out = String::with_capacity(text.len());
for c in text.chars() {
if takes_a_backslash(c, delim) {
out.push('\\');
}
out.push(c);
}
out
}
pub(crate) fn cleanup(raw: &[Traced], delim: Option<char>) -> Vec<Traced> {
let (mut out, end) = cleanup_written(raw, delim);
out.truncate(end.unwrap_or(0));
out
}
fn cleanup_written(raw: &[Traced], delim: Option<char>) -> (Vec<Traced>, Option<usize>) {
let s = skip_spaces(raw);
let mut out = Vec::with_capacity(s.len());
let mut end = None;
let mut inside_quotes = false;
let mut i = 0;
while i < s.len() {
let (c, start, _) = s[i];
let escaped = match (c, s.get(i + 1)) {
('\\', Some(&(next, _, next_end))) => {
unescape(next, delim).map(|e| (e, start, next_end))
}
_ => None,
};
if let Some(e) = escaped {
out.push(e);
end = Some(out.len());
i += 2;
continue;
}
if c == '\'' && (inside_quotes || has_quote(&s[i + 1..])) {
inside_quotes = !inside_quotes;
if inside_quotes {
end = Some(out.len());
}
} else {
out.push(s[i]);
if c != ' ' || inside_quotes {
end = Some(out.len());
}
}
i += 1;
}
(out, end)
}
#[cfg(feature = "esl")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Head {
Picked(char),
Unreadable,
Absent,
}
#[cfg(feature = "esl")]
pub(crate) fn argument_head(text: &[Traced]) -> Head {
let at = |index: usize| {
text.get(index)
.map_or('\0', |&(c, ..)| c)
};
match (at(0), at(1), at(2)) {
('^', '^', picked) if picked != '\0' && !picked.is_ascii() => Head::Unreadable,
('^', '^', picked) if picked != '\0' && at(3) != '\0' => Head::Picked(picked),
_ => Head::Absent,
}
}
#[cfg(feature = "esl")]
pub(crate) struct CBuffer(Vec<Traced>);
#[cfg(feature = "esl")]
const CONTINUATION: char = '\u{80}';
pub(crate) fn is_continuation(&(_, start, end): &Traced) -> bool {
start > end
}
#[cfg(feature = "esl")]
pub(crate) struct Split {
pub(crate) tokens: Vec<SplitToken>,
pub(crate) delimiter: char,
pub(crate) held_delimiter: bool,
pub(crate) open_quote: bool,
pub(crate) unreadable_head: bool,
}
#[cfg(feature = "esl")]
pub(crate) struct SplitToken {
pub(crate) raw: Range<usize>,
pub(crate) start: usize,
}
#[cfg(feature = "esl")]
impl CBuffer {
pub(crate) fn new(text: &[Traced]) -> Self {
Self::with_origins(text).0
}
fn with_origins(text: &[Traced]) -> (Self, Vec<usize>) {
let end = extent(text).end;
let mut chars = Vec::with_capacity(text.len() + 2);
let mut origins = Vec::with_capacity(text.len() + 2);
for (at, &entry) in text
.iter()
.enumerate()
{
chars.push(entry);
origins.push(at);
let (c, start, end) = entry;
let continued = text
.get(at + 1)
.is_some_and(is_continuation);
if start < end && !continued {
for _ in 1..c.len_utf8() {
chars.push((CONTINUATION, end + 1, end));
origins.push(at + 1);
}
}
}
chars.extend([('\0', end, end), ('\0', end, end)]);
origins.extend([text.len(), text.len()]);
(Self(chars), origins)
}
pub(crate) fn at(&self, index: usize) -> char {
self.0
.get(index)
.map_or('\0', |&(c, ..)| c)
}
pub(crate) fn terminate(&mut self, index: usize) {
if let Some(c) = self
.0
.get_mut(index)
{
c.0 = '\0';
}
}
pub(crate) fn c_str(&self, index: usize) -> &[Traced] {
let rest = self
.0
.get(index..)
.unwrap_or_default();
let len = rest
.iter()
.position(|&(c, ..)| c == '\0')
.unwrap_or(rest.len());
&rest[..len]
}
pub(crate) fn c_str_mut(&mut self, index: usize) -> &mut [Traced] {
let len = self
.c_str(index)
.len();
&mut self.0[index..index + len]
}
pub(crate) fn into_chars(self) -> Vec<Traced> {
self.0
}
pub(crate) fn separate(&mut self, index: usize, delim: char, limit: usize) -> Split {
match argument_head(self.c_str(index)) {
Head::Picked(picked) => self.split(index + 3, picked, limit),
Head::Unreadable => Split {
unreadable_head: true,
..self.split(index, delim, limit)
},
Head::Absent => self.split(index, delim, limit),
}
}
fn split(&mut self, index: usize, delim: char, limit: usize) -> Split {
let text = &self.0[index.min(
self.0
.len(),
)..];
let cut = match (limit, delim) {
(0, _) => Cut {
spans: Vec::new(),
held_delimiter: false,
open_quote: false,
},
(_, ' ') => blank_delim(text, limit),
(_, delim) => char_delim(text, delim, limit),
};
for span in &cut.spans {
if self.at(index + span.end) == delim {
self.terminate(index + span.end);
}
}
let cleanup_delim = (delim != ' ').then_some(delim);
let tokens = cut
.spans
.iter()
.map(|span| SplitToken {
raw: index + span.start..index + span.end,
start: self.cleanup(index + span.start, cleanup_delim),
})
.collect();
Split {
tokens,
delimiter: delim,
held_delimiter: cut.held_delimiter,
open_quote: cut.open_quote,
unreadable_head: false,
}
}
fn cleanup(&mut self, index: usize, delim: Option<char>) -> usize {
let raw = self
.c_str(index)
.to_vec();
let start = index + (raw.len() - skip_spaces(&raw).len());
let (written, end) = cleanup_written(&raw, delim);
self.0[start..start + written.len()].copy_from_slice(&written);
if let Some(end) = end {
self.terminate(start + end);
}
start
}
}
#[cfg(feature = "esl")]
pub(crate) struct Token {
pub(crate) raw: Range<usize>,
pub(crate) text: Vec<Traced>,
}
#[cfg(feature = "esl")]
pub(crate) struct Separated {
pub(crate) tokens: Vec<Token>,
pub(crate) delimiter: char,
pub(crate) held_delimiter: bool,
pub(crate) open_quote: bool,
}
#[cfg(feature = "esl")]
pub(crate) fn separate(text: &[Traced], delim: char, limit: usize) -> Separated {
read_back(text, |buffer| buffer.separate(0, delim, limit))
}
#[cfg(feature = "esl")]
pub(crate) fn separate_on(text: &[Traced], delim: char, limit: usize) -> Separated {
read_back(text, |buffer| buffer.split(0, delim, limit))
}
#[cfg(feature = "esl")]
fn read_back(text: &[Traced], cut: impl FnOnce(&mut CBuffer) -> Split) -> Separated {
let (mut buffer, origins) = CBuffer::with_origins(text);
let split = cut(&mut buffer);
let within = |at: usize| {
origins
.get(at)
.map_or(text.len(), |&origin| origin.min(text.len()))
};
Separated {
tokens: split
.tokens
.iter()
.map(|token| Token {
raw: within(
token
.raw
.start,
)
..within(
token
.raw
.end,
),
text: buffer
.c_str(token.start)
.iter()
.copied()
.filter(|entry| !is_continuation(entry))
.collect(),
})
.collect(),
delimiter: split.delimiter,
held_delimiter: split.held_delimiter,
open_quote: split.open_quote,
}
}
#[cfg(feature = "esl")]
pub(crate) fn find_end_paren(text: &[Traced], open: char, close: char) -> Option<usize> {
let skip = text.len() - skip_spaces(text).len();
if text
.get(skip)?
.0
!= open
{
return None;
}
let mut depth = 1usize;
for (i, &(c, ..)) in text
.iter()
.enumerate()
.skip(skip + 1)
{
if c == open && open != close {
depth += 1;
} else if c == close {
depth -= 1;
if depth == 0 {
return Some(i);
}
}
}
None
}
#[cfg(feature = "esl")]
pub(crate) fn find(text: &[Traced], needle: &str) -> Option<usize> {
let needle: Vec<char> = needle
.chars()
.collect();
let last = text
.len()
.checked_sub(needle.len())?;
(0..=last).find(|&i| {
text[i..i + needle.len()]
.iter()
.map(|&(c, ..)| c)
.eq(needle
.iter()
.copied())
})
}
#[cfg(feature = "esl")]
pub(crate) fn separate_string_string(
text: &[Traced],
delim: &str,
limit: usize,
) -> Vec<Range<usize>> {
let width = delim
.chars()
.count();
let mut spans = Vec::new();
let mut start = 0;
while spans.len() + 1 < limit {
let Some(at) = find(&text[start..], delim) else {
break;
};
spans.push(start..start + at);
start += at + width;
}
spans.push(start..text.len());
spans
}
#[cfg(feature = "sdp")]
pub(crate) fn separate_string_char_delim(s: &str, delim: char) -> Vec<String> {
let text = trace(s);
char_delim(&text, delim, usize::MAX)
.spans
.into_iter()
.map(|span| untrace(&cleanup(&text[span], Some(delim))))
.collect()
}
#[cfg(all(test, feature = "esl"))]
mod c_oracle;
#[cfg(all(test, feature = "esl"))]
mod tiling {
use super::*;
use crate::switch_passes::{byte_range, trace};
use crate::test_text::TILING_INPUTS;
#[test]
fn tokens_meet_only_at_their_delimiters() {
for input in TILING_INPUTS {
let text = trace(input);
for delim in [' ', ',', '|', '='] {
let ranges: Vec<_> = separate(&text, delim, usize::MAX)
.tokens
.into_iter()
.map(|token| byte_range(&text, extent(&text), token.raw))
.collect();
let is_delimiter = |gap: &str| match delim {
' ' => {
!gap.is_empty()
&& gap
.chars()
.all(|c| c == ' ')
}
_ => gap.len() == 1 && gap.starts_with(delim),
};
let context = format!("{input:?} on {delim:?}: {ranges:?}");
let mut at = 0;
for (k, range) in ranges
.iter()
.enumerate()
{
let gap = &input[at..range.start];
assert!(
if k == 0 {
gap.is_empty()
} else {
is_delimiter(gap)
},
"{context}"
);
at = range.end;
}
let rest = &input[at..];
assert!(rest.is_empty() || is_delimiter(rest), "{context}");
}
}
}
}
#[cfg(all(test, feature = "sdp"))]
mod tests {
use super::*;
#[test]
fn odd_quote_count_before_comma_still_splits() {
let tokens = separate_string_char_delim("a'b'c'd,PCMA", ',');
assert_eq!(tokens.len(), 2);
assert_eq!(tokens[1], "PCMA");
}
#[test]
fn escape_of_delimiter_follows_the_delimiter() {
assert_eq!(
separate_string_char_delim(r"a\|b|c\,d", '|'),
vec!["a|b".to_string(), r"c\,d".to_string()]
);
}
}