use crate::ruby_strip::RubyStrip;
use crate::unicode_word::is_word;
use std::ops::Range;
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct Field {
pub index: usize,
pub name: Option<Range<usize>>,
pub value: Option<Range<usize>>,
pub quote: Option<char>,
pub single_quoted: bool,
pub gap: Option<Range<usize>>,
}
impl Field {
pub fn text(&self, src: &str) -> Option<String> {
let range = self.value.clone()?;
let mut out = String::with_capacity(range.len());
let mut chars = src[range.clone()].char_indices().peekable();
while let Some((i, c)) = chars.next() {
let at = range.start + i;
if self.gap.as_ref().is_some_and(|gap| gap.contains(&at)) {
out.push(' ');
} else if c == '\\' && self.quote.is_some() && chars.peek().map(|p| p.1) == self.quote {
out.push(chars.next().unwrap().1);
} else {
out.push(c);
}
}
Some(out)
}
pub fn name<'a>(&self, src: &'a str) -> Option<&'a str> {
self.name.clone().map(|range| &src[range])
}
}
pub(crate) fn parse(src: &str) -> Vec<Field> {
let mut scanner = Scanner { src, at: 0 };
let mut fields = Vec::new();
let mut index = 0;
while let Some((field, more)) = scanner.attribute(index) {
fields.extend(field);
if !more || scanner.at >= src.len() {
break;
}
scanner.skip_delimiter();
index += 1;
}
fields
}
struct Scanner<'a> {
src: &'a str,
at: usize,
}
impl Scanner<'_> {
fn peek(&self) -> Option<char> {
self.src[self.at..].chars().next()
}
fn skip_blank(&mut self) -> usize {
let start = self.at;
while matches!(self.peek(), Some(' ' | '\t')) {
self.at += 1;
}
self.at - start
}
fn skip_delimiter(&mut self) {
let start = self.at;
self.skip_blank();
match self.peek() {
Some(',') => self.at += 1,
None | Some('\n') => {}
_ => self.at = start,
}
}
fn scan_name(&mut self) -> Option<Range<usize>> {
let start = self.at;
let mut chars = self.src[start..].char_indices();
match chars.next() {
Some((_, c)) if is_word(c) => {}
_ => return None,
}
let end = chars
.find(|(_, c)| !is_word(*c) && !matches!(c, '-' | '.'))
.map_or(self.src.len(), |(i, _)| start + i);
self.at = end;
Some(start..end)
}
fn scan_to_delimiter(&mut self) -> usize {
let rest = &self.src[self.at..];
let stop = rest.find([',', '\n']).unwrap_or(rest.len());
self.at += rest[..stop].trim_end_matches([' ', '\t']).len();
self.at
}
fn value(&mut self, quote: char) -> (Range<usize>, bool) {
let start = self.at;
if self.peek() == Some(quote) {
self.at += 1;
return (start..start, true);
}
let bytes = self.src.as_bytes();
let mut i = start;
while i < bytes.len() && bytes[i] != b'\n' {
if bytes[i] == quote as u8 && i > start && bytes[i - 1] != b'\\' {
self.at = i + 1;
return (start..i, true);
}
i += 1;
}
let end = self.scan_to_delimiter();
(start - 1..end, false)
}
fn quoted(&mut self, index: usize, name: Option<Range<usize>>) -> Field {
let quote = self.peek().unwrap();
self.at += 1;
let (value, closed) = self.value(quote);
Field {
index,
name,
single_quoted: closed && quote == '\'' && {
let raw = &self.src[value.clone()];
!(raw.starts_with('\'') || raw.starts_with("\\'"))
},
quote: closed.then_some(quote),
value: Some(value),
gap: None,
}
}
fn attribute(&mut self, index: usize) -> Option<(Option<Field>, bool)> {
self.skip_blank();
if matches!(self.peek(), Some('"' | '\'')) {
return Some((Some(self.quoted(index, None)), true));
}
let name = self.scan_name();
let skipped = if name.is_some() { self.skip_blank() } else { 0 };
let positional = |value| Field {
index,
name: None,
value,
quote: None,
single_quoted: false,
gap: None,
};
let Some(c) = self.peek().filter(|c| *c != '\n') else {
if name.is_none() && !self.src.ruby_rstrip().ends_with(',') {
return None;
}
return Some((Some(positional(name)), false));
};
if c == ',' {
return Some((Some(positional(name)), true));
}
let Some(name) = name else {
let start = self.at;
self.at += c.len_utf8();
let end = self.scan_to_delimiter();
return Some((Some(positional(Some(start..end))), true));
};
if c != '=' {
let gap = name.end..name.end + skipped;
let end = self.scan_to_delimiter();
let mut field = positional(Some(name.start..end));
field.gap = (skipped > 0).then_some(gap);
return Some((Some(field), true));
}
self.at += 1;
self.skip_blank();
let named = |value| Field {
index,
name: Some(name.clone()),
value: Some(value),
quote: None,
single_quoted: false,
gap: None,
};
let field = match self.peek() {
Some('"' | '\'') => self.quoted(index, Some(name.clone())),
Some(',') | Some('\n') | None => named(self.at..self.at),
Some(c) => {
let start = self.at;
self.at += c.len_utf8();
let end = self.scan_to_delimiter();
if &self.src[start..end] == "None" {
return Some((None, true));
}
named(start..end)
}
};
Some((Some(field), true))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn values(src: &str) -> Vec<(usize, Option<String>, Option<String>, bool)> {
parse(src)
.iter()
.map(|f| {
(
f.index,
f.name(src).map(str::to_owned),
f.text(src),
f.single_quoted,
)
})
.collect()
}
#[test]
fn positional_indexes_count_named_and_skipped_fields() {
let v = values("quote, role=x, Author");
assert_eq!(v[0], (0, None, Some("quote".into()), false));
assert_eq!(v[1], (1, Some("role".into()), Some("x".into()), false));
assert_eq!(v[2], (2, None, Some("Author".into()), false));
let v = values("quote, role=None, A");
assert_eq!(v.len(), 2);
assert_eq!(v[1], (2, None, Some("A".into()), false));
}
#[test]
fn empty_fields_are_nil_and_quoted_empties_are_strings() {
let v = values("quote, , W");
assert_eq!(v[1], (1, None, None, false));
assert_eq!(v[2].2.as_deref(), Some("W"));
let v = values("quote,\"\"");
assert_eq!(v[1].2.as_deref(), Some(""));
let v = values("quote, ");
assert_eq!(v.len(), 2);
assert_eq!(v[1].2, None);
assert_eq!(values("quote").len(), 1);
}
#[test]
fn quotes_escape_and_recover() {
let v = values(r#"quote, "A\"q", W"#);
assert_eq!(v[1].2.as_deref(), Some("A\"q"));
let v = values("quote, \"A, B");
assert_eq!(v[1].2.as_deref(), Some("\"A"));
assert_eq!(v[2].2.as_deref(), Some("B"));
let v = values("'a', '''b', 'c");
assert!(v[0].3);
assert_eq!(v[1].2.as_deref(), Some(""));
assert!(v[1].3);
assert_eq!((v[2].0, v[2].2.as_deref(), v[2].3), (2, Some("b"), true));
assert_eq!(v[3].2.as_deref(), Some("'c"));
assert!(!v[3].3);
let v = values("x, ''''");
assert_eq!(v[1].2.as_deref(), Some(""));
assert_eq!(values("x ''y'")[0].2.as_deref(), Some("x ''y'"));
}
#[test]
fn adjacent_fields_and_gaps_follow_ruby() {
let v = values("\"quote\" A,B");
assert_eq!(
v.iter().map(|f| f.2.clone().unwrap()).collect::<Vec<_>>(),
["quote", "A", "B"]
);
assert_eq!(v[1].0, 1);
assert_eq!(values("A\tB, C")[0].2.as_deref(), Some("A B"));
assert_eq!(values("#a.b, x")[0].2.as_deref(), Some("#a.b"));
assert_eq!(values("a=, b=")[0].2.as_deref(), Some(""));
assert_eq!(values("a=, b=")[1].2.as_deref(), Some(""));
assert_eq!(values("Ünï=ß")[0].1.as_deref(), Some("Ünï"));
}
}