use crate::{
de::{Position, Span},
error::SpannedResult,
};
use alloc::string::String;
impl Position {
#[must_use]
pub fn grapheme_index(&self, s: &str) -> Option<usize> {
use unicode_segmentation::UnicodeSegmentation;
let mut line_no = 1;
let mut col_no = 1;
if (self.line, self.col) == (1, 1) {
return Some(0);
}
let mut i = 0;
if (line_no, col_no) == (self.line, self.col) {
return Some(i);
}
for ch in s.graphemes(true) {
if (line_no, col_no) == (self.line, self.col) {
return Some(i);
}
if matches!(ch, "\n" | "\r\n") {
line_no += 1;
col_no = 1;
} else {
col_no += 1;
}
i += 1;
}
if (line_no, col_no) == (self.line, self.col) {
return Some(i);
}
None
}
}
impl Span {
#[must_use]
pub fn substring_exclusive(&self, s: &str) -> Option<String> {
use alloc::vec::Vec;
use unicode_segmentation::UnicodeSegmentation;
if let (Some(start), Some(end)) = (self.start.grapheme_index(s), self.end.grapheme_index(s))
{
Some(s.graphemes(true).collect::<Vec<&str>>()[start..end].concat())
} else {
None
}
}
#[must_use]
pub fn substring_inclusive(&self, s: &str) -> Option<String> {
use alloc::vec::Vec;
use unicode_segmentation::UnicodeSegmentation;
if let (Some(start), Some(end)) = (self.start.grapheme_index(s), self.end.grapheme_index(s))
{
Some(s.graphemes(true).collect::<Vec<&str>>()[start..=end].concat())
} else {
None
}
}
}
#[allow(clippy::unwrap_used)]
#[allow(clippy::missing_panics_doc)]
pub fn check_error_span_exclusive<T: serde::de::DeserializeOwned + PartialEq + core::fmt::Debug>(
ron: &str,
check: SpannedResult<T>,
substr: &str,
) {
let res_str = crate::de::from_str::<T>(ron);
assert_eq!(res_str, check);
let res_bytes = crate::de::from_bytes::<T>(ron.as_bytes());
assert_eq!(res_bytes, check);
#[cfg(feature = "std")]
{
let res_reader = crate::de::from_reader::<&[u8], T>(ron.as_bytes());
assert_eq!(res_reader, check);
}
assert_eq!(
check.unwrap_err().span.substring_exclusive(ron).unwrap(),
substr
);
}
#[allow(clippy::unwrap_used)]
#[allow(clippy::missing_panics_doc)]
pub fn check_error_span_inclusive<T: serde::de::DeserializeOwned + PartialEq + core::fmt::Debug>(
ron: &str,
check: SpannedResult<T>,
substr: &str,
) {
let res_str = crate::de::from_str::<T>(ron);
assert_eq!(res_str, check);
let res_bytes = crate::de::from_bytes::<T>(ron.as_bytes());
assert_eq!(res_bytes, check);
#[cfg(feature = "std")]
{
let res_reader = crate::de::from_reader::<&[u8], T>(ron.as_bytes());
assert_eq!(res_reader, check);
}
assert_eq!(
check.unwrap_err().span.substring_inclusive(ron).unwrap(),
substr
);
}
#[cfg(test)]
mod tests {
use crate::de::{Position, Span};
fn span(start: Position, end: Position) -> Span {
Span { start, end }
}
fn pos(line: usize, col: usize) -> Position {
Position { line, col }
}
#[test]
fn ascii_basics() {
let text = "hello\nworld";
assert_eq!(pos(1, 1).grapheme_index(text), Some(0));
assert_eq!(pos(1, 5).grapheme_index(text), Some(4));
assert_eq!(pos(2, 1).grapheme_index(text), Some(6));
assert_eq!(
span(pos(1, 4), pos(2, 2))
.substring_exclusive(text)
.unwrap(),
"lo\nw"
);
}
#[test]
fn multibyte_greek() {
let text = "αβγ\ndeux\n三四五\r\nend";
assert_eq!(pos(1, 2).grapheme_index(text), Some(1));
assert_eq!(pos(3, 1).grapheme_index(text), Some(9));
assert_eq!(pos(4, 1).grapheme_index(text), Some(13));
assert_eq!(
span(pos(1, 1), pos(2, 1))
.substring_exclusive(text)
.unwrap(),
"αβγ\n"
);
}
#[test]
fn combining_mark_cluster() {
let text = "e\u{0301}x\n";
assert_eq!(pos(1, 1).grapheme_index(text), Some(0));
assert_eq!(pos(1, 2).grapheme_index(text), Some(1));
assert_eq!(pos(1, 4).grapheme_index(text), None);
assert_eq!(
span(pos(1, 1), pos(1, 2))
.substring_exclusive(text)
.unwrap(),
"e\u{0301}"
);
}
#[test]
fn zwj_emoji_cluster() {
let text = "👩👩👧👧 and 👨👩👦";
assert_eq!(pos(1, 1).grapheme_index(text), Some(0));
assert_eq!(pos(1, 2).grapheme_index(text), Some(1));
assert_eq!(
span(pos(1, 1), pos(1, 2))
.substring_exclusive(text)
.unwrap(),
"👩👩👧👧"
);
assert_eq!(
span(pos(1, 7), pos(1, 8))
.substring_exclusive(text)
.unwrap(),
"👨👩👦"
);
}
#[test]
fn mixed_newlines() {
let text = "one\r\ntwo\nthree\r\n";
assert_eq!(pos(2, 1).grapheme_index(text), Some(4));
assert_eq!(pos(3, 1).grapheme_index(text), Some(8));
assert_eq!(
span(pos(2, 1), pos(3, 1))
.substring_exclusive(text)
.unwrap(),
"two\n"
);
assert_eq!(
span(pos(2, 1), pos(3, 6))
.substring_exclusive(text)
.unwrap(),
"two\nthree"
);
}
#[test]
fn oob_and_error_paths() {
let text = "short";
assert_eq!(pos(2, 1).grapheme_index(text), None);
assert_eq!(pos(1, 10).grapheme_index(text), None);
assert_eq!(span(pos(1, 1), pos(2, 1)).substring_exclusive(text), None);
}
#[test]
fn whole_text_span() {
let text = "αβγ\nδεζ";
let all = span(pos(1, 1), pos(2, 4));
assert_eq!(&all.substring_exclusive(text).unwrap(), text);
}
#[test]
fn span_substring_helper() {
assert_eq!(
Span {
start: Position { line: 1, col: 1 },
end: Position { line: 2, col: 1 },
}
.substring_exclusive(
"In the first place, there are two sorts of bets, or toh.11 There is the
single axial bet in the center between the principals (toh ketengah), and
there is the cloud of peripheral ones around the ring between members
of the audience (toh kesasi). ",
)
.unwrap(),
"In the first place, there are two sorts of bets, or toh.11 There is the\n"
);
assert_eq!(
Span {
start: Position { line: 2, col: 1 },
end: Position { line: 3, col: 1 },
}
.substring_exclusive(
"In the first place, there are two sorts of bets, or toh.11 There is the
single axial bet in the center between the principals (toh ketengah), and
there is the cloud of peripheral ones around the ring between members
of the audience (toh kesasi). ",
)
.unwrap(),
"single axial bet in the center between the principals (toh ketengah), and\n"
);
}
}