use super::error::{Error, ErrorKind, Location, Position};
#[must_use]
pub fn is_printable(character: char) -> bool {
matches!(character,
'\u{20}'..='\u{7E}'
| '\u{A0}'..='\u{D7FF}'
| '\u{E000}'..='\u{FFFD}'
| '\u{10000}'..='\u{10FFFF}')
&& !matches!(character, '\u{2028}' | '\u{2029}' | '\u{FEFF}')
}
pub(super) fn decode(bytes: &[u8]) -> Result<&str, Error> {
let text = std::str::from_utf8(bytes).map_err(|error| Error {
location: Location::Byte(error.valid_up_to()),
kind: ErrorKind::InvalidUtf8,
})?;
let mut line = 1;
let mut column = 1;
for character in text.chars() {
let position = Position::new(line, column);
match character {
'\n' => {
line += 1;
column = 1;
continue;
}
'\t' => {}
'\u{FEFF}' => return Err(Error::at(position, ErrorKind::ByteOrderMark)),
'\r' => return Err(Error::at(position, ErrorKind::CarriageReturn)),
other if !is_printable(other) => {
return Err(Error::at(position, ErrorKind::NotPrintable(other.into())));
}
_ => {}
}
column += 1;
}
Ok(text)
}
#[derive(Debug, Clone, Copy)]
pub(super) struct Line<'a> {
pub number: usize,
pub column: usize,
pub indent: usize,
pub text: &'a str,
pub raw: &'a str,
}
impl<'a> Line<'a> {
pub fn split(text: &'a str) -> Vec<Self> {
let mut lines: Vec<Self> = text
.split('\n')
.enumerate()
.map(|(index, raw)| {
let indent = leading_spaces(raw);
Self {
number: index + 1,
column: indent + 1,
indent,
text: &raw[indent..],
raw,
}
})
.collect();
if lines.last().is_some_and(|line| line.raw.is_empty()) {
lines.pop();
}
lines
}
pub const fn is_blank(&self) -> bool {
self.text.is_empty()
}
pub fn position(&self, offset: usize) -> Position {
Position::new(
self.number,
self.column + self.text[..offset].chars().count(),
)
}
pub fn start(&self) -> Position {
self.position(0)
}
}
pub(super) fn leading_spaces(text: &str) -> usize {
text.len() - text.trim_start_matches(' ').len()
}