use std::ops::Range;
pub(crate) type Traced = (char, usize);
pub(crate) fn trace(s: &str) -> Vec<Traced> {
s.char_indices()
.map(|(at, c)| (c, at))
.collect()
}
pub(crate) fn untrace(text: &[Traced]) -> String {
text.iter()
.map(|&(c, _)| c)
.collect()
}
#[cfg(feature = "esl")]
pub(crate) fn byte_range(text: &[Traced], span: Range<usize>) -> Range<usize> {
let end_of_text = text
.last()
.map_or(0, |&(c, at)| at + c.len_utf8());
let start = text
.get(span.start)
.map_or(end_of_text, |&(_, at)| at);
match text[span].last() {
Some(&(c, at)) => start..at + c.len_utf8(),
None => start..start,
}
}
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()
.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 i = 0;
while i < text.len() {
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..text.len());
}
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 i = 0;
while i < text.len() {
let c = text[i].0;
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..text.len());
}
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 cleanup(raw: &[Traced], delim: Option<char>) -> Vec<Traced> {
let s = skip_spaces(raw);
let mut out = Vec::with_capacity(s.len());
let mut end = 0;
let mut inside_quotes = false;
let mut i = 0;
while i < s.len() {
let (c, at) = s[i];
let escaped = match (c, s.get(i + 1)) {
('\\', Some(&(next, _))) => unescape(next, delim),
_ => None,
};
if let Some(e) = escaped {
out.push((e, at));
end = out.len();
i += 2;
continue;
}
if c == '\'' && (inside_quotes || has_quote(&s[i + 1..])) {
inside_quotes = !inside_quotes;
if inside_quotes {
end = out.len();
}
} else {
out.push((c, at));
if c != ' ' || inside_quotes {
end = out.len();
}
}
i += 1;
}
out.truncate(end);
out
}
#[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) held_delimiter: bool,
pub(crate) open_quote: bool,
}
#[cfg(feature = "esl")]
pub(crate) fn separate(text: &[Traced], delim: char, limit: usize) -> Separated {
let (skipped, delim) = match text {
[('^', _), ('^', _), (picked, _), _, ..] => (3, *picked),
_ => (0, delim),
};
let body = &text[skipped..];
let (cut, cleanup_delim) = match (limit, delim) {
(0, _) => (
Cut {
spans: Vec::new(),
held_delimiter: false,
open_quote: false,
},
None,
),
(_, ' ') => (blank_delim(body, limit), None),
(_, delim) => (char_delim(body, delim, limit), Some(delim)),
};
Separated {
tokens: cut
.spans
.into_iter()
.map(|span| Token {
text: cleanup(&body[span.clone()], cleanup_delim),
raw: span.start + skipped..span.end + skipped,
})
.collect(),
held_delimiter: cut.held_delimiter,
open_quote: cut.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(feature = "esl")]
pub(crate) fn char_delim_spans(s: &str, delim: char) -> Vec<&str> {
let text = trace(s);
char_delim(&text, delim, usize::MAX)
.spans
.into_iter()
.map(|span| &s[byte_range(&text, span)])
.collect()
}
#[cfg(feature = "esl")]
pub(crate) fn blank_delim_spans(s: &str) -> (Vec<&str>, bool) {
let text = trace(s);
let cut = blank_delim(&text, usize::MAX);
let spans = cut
.spans
.into_iter()
.map(|span| &s[byte_range(&text, span)])
.collect();
(spans, cut.open_quote)
}
#[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()]
);
}
}