use alloc::{borrow::Cow, string::String, vec::Vec};
use core::{fmt, ops::Range};
use crate::{Diagnostic, DiagnosticCode, EditError, LineIndex, PositionError};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Quote {
None,
Single,
Double,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum AssignmentState {
Bare,
Empty,
Value,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum LineEnding {
None,
Lf,
CrLf,
Cr,
}
impl LineEnding {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::None => "",
Self::Lf => "\n",
Self::CrLf => "\r\n",
Self::Cr => "\r",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ItemKind {
Blank,
Comment,
Entry,
Invalid,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EntryTokens {
pub indent: Range<usize>,
pub export: Option<Range<usize>>,
pub space_after_export: Range<usize>,
pub key: Range<usize>,
pub space_before_eq: Range<usize>,
pub eq: Option<Range<usize>>,
pub space_after_eq: Range<usize>,
pub value: Option<Range<usize>>,
pub body: Option<Range<usize>>,
pub trailing_space: Range<usize>,
pub inline_comment: Option<Range<usize>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct DecodeSegment {
pub decoded: Range<usize>,
pub raw: Range<usize>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct EntryData {
pub item: usize,
pub line: usize,
pub tokens: EntryTokens,
pub quote: Quote,
pub state: AssignmentState,
pub decoded: Option<String>,
pub map: Vec<DecodeSegment>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ItemData {
pub kind: ItemKind,
pub span: Range<usize>,
pub ending: LineEnding,
pub entry: Option<usize>,
pub comment: Option<Range<usize>>,
pub invalid: Option<Diagnostic>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Document<'src> {
pub(crate) source: Cow<'src, str>,
pub(crate) items: Vec<ItemData>,
pub(crate) entries: Vec<EntryData>,
pub(crate) bom: bool,
}
impl Document<'_> {
#[must_use]
pub fn source(&self) -> &str {
&self.source
}
pub fn items(&self) -> impl DoubleEndedIterator<Item = Item<'_>> + ExactSizeIterator {
(0..self.items.len()).map(|index| Item {
document: self,
index,
})
}
#[must_use]
pub fn item(&self, index: usize) -> Option<Item<'_>> {
(index < self.items.len()).then_some(Item {
document: self,
index,
})
}
pub fn entries(&self) -> impl DoubleEndedIterator<Item = Entry<'_>> + ExactSizeIterator {
(0..self.entries.len()).map(|index| Entry {
document: self,
index,
})
}
#[must_use]
pub fn entry(&self, index: usize) -> Option<Entry<'_>> {
(index < self.entries.len()).then_some(Entry {
document: self,
index,
})
}
pub fn get_all<'doc, 'key>(
&'doc self,
key: &'key str,
) -> impl DoubleEndedIterator<Item = Entry<'doc>> + 'key
where
'doc: 'key,
{
self.entries().filter(move |entry| entry.key() == key)
}
#[must_use]
pub fn get_entry(&self, key: &str) -> Option<Entry<'_>> {
self.entries().rev().find(|entry| entry.key() == key)
}
#[must_use]
pub fn get(&self, key: &str) -> Option<&str> {
self.get_entry(key).and_then(Entry::value)
}
pub fn unique(&self, key: &str) -> Result<Entry<'_>, EditError> {
let mut entries = self.entries().filter(|entry| entry.key() == key);
let first = entries.next().ok_or(EditError::Missing)?;
let remaining = entries.count();
if remaining == 0 {
Ok(first)
} else {
Err(EditError::Duplicate(remaining + 1))
}
}
#[must_use]
pub const fn has_bom(&self) -> bool {
self.bom
}
#[must_use]
pub fn bom_span(&self) -> Option<Range<usize>> {
self.bom.then_some(0..3)
}
#[must_use]
pub fn has_final_newline(&self) -> bool {
self.source.ends_with(['\n', '\r'])
}
#[must_use]
pub fn line_index(&self) -> LineIndex<'_> {
LineIndex::new(self.source())
}
#[must_use]
pub fn into_owned(self) -> Document<'static> {
Document {
source: Cow::Owned(self.source.into_owned()),
items: self.items,
entries: self.entries,
bom: self.bom,
}
}
pub(crate) fn preferred_ending(&self) -> &'static str {
let mut counts = [0usize; 3];
let bytes = self.source.as_bytes();
let mut at = 0;
while at < bytes.len() {
match bytes[at] {
b'\r' if bytes.get(at + 1) == Some(&b'\n') => {
counts[1] += 1;
at += 1;
}
b'\r' => counts[2] += 1,
b'\n' => counts[0] += 1,
_ => {}
}
at += 1;
}
if counts[1] > counts[0] && counts[1] >= counts[2] {
"\r\n"
} else if counts[2] > counts[0] && counts[2] > counts[1] {
"\r"
} else {
"\n"
}
}
}
impl fmt::Display for Document<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.source())
}
}
#[derive(Debug, Clone, Copy)]
pub struct Entry<'doc> {
pub(crate) document: &'doc Document<'doc>,
pub(crate) index: usize,
}
impl<'doc> Entry<'doc> {
fn data(self) -> &'doc EntryData {
&self.document.entries[self.index]
}
#[must_use]
pub const fn id(self) -> usize {
self.index
}
#[must_use]
pub fn item_id(self) -> usize {
self.data().item
}
#[must_use]
pub fn key(self) -> &'doc str {
&self.document.source[self.data().tokens.key.clone()]
}
#[must_use]
pub fn value(self) -> Option<&'doc str> {
let data = self.data();
data.tokens.body.as_ref().map(|body| {
data.decoded
.as_deref()
.unwrap_or(&self.document.source[body.clone()])
})
}
#[must_use]
pub fn raw_value(self) -> Option<&'doc str> {
self.data()
.tokens
.value
.as_ref()
.map(|span| &self.document.source[span.clone()])
}
#[must_use]
pub fn quote(self) -> Quote {
self.data().quote
}
#[must_use]
pub fn exported(self) -> bool {
self.data().tokens.export.is_some()
}
#[must_use]
pub fn state(self) -> AssignmentState {
self.data().state
}
#[must_use]
pub fn line(self) -> usize {
self.data().line
}
#[must_use]
pub fn line_span(self) -> Range<usize> {
self.document.items[self.item_id()].span.clone()
}
#[must_use]
pub fn key_span(self) -> Range<usize> {
self.data().tokens.key.clone()
}
#[must_use]
pub fn value_span(self) -> Option<Range<usize>> {
self.data().tokens.value.clone()
}
#[must_use]
pub fn tokens(self) -> &'doc EntryTokens {
&self.data().tokens
}
#[must_use]
pub fn inline_comment(self) -> Option<&'doc str> {
self.data()
.tokens
.inline_comment
.as_ref()
.map(|span| &self.document.source[span.clone()])
}
pub fn decoded_offset_to_raw(self, offset: usize) -> Result<usize, PositionError> {
let value = self.value().ok_or(PositionError::OutOfBounds)?;
if offset > value.len() {
return Err(PositionError::OutOfBounds);
}
if !value.is_char_boundary(offset) {
return Err(PositionError::InvalidUtf8Boundary);
}
let data = self.data();
let body = data
.tokens
.body
.as_ref()
.ok_or(PositionError::OutOfBounds)?;
let count = data
.map
.partition_point(|segment| segment.decoded.start <= offset);
let Some(index) = count.checked_sub(1) else {
return Ok(body.start + offset);
};
let segment = &data.map[index];
if offset == segment.decoded.start {
return Ok(segment.raw.start);
}
if offset < segment.decoded.end {
return Err(PositionError::InvalidUtf8Boundary);
}
Ok(segment.raw.end + offset - segment.decoded.end)
}
pub fn decoded_span_to_raw(self, span: Range<usize>) -> Result<Range<usize>, PositionError> {
if span.start > span.end {
return Err(PositionError::OutOfBounds);
}
Ok(self.decoded_offset_to_raw(span.start)?..self.decoded_offset_to_raw(span.end)?)
}
}
#[derive(Debug, Clone, Copy)]
pub struct Item<'doc> {
pub(crate) document: &'doc Document<'doc>,
pub(crate) index: usize,
}
impl<'doc> Item<'doc> {
fn data(self) -> &'doc ItemData {
&self.document.items[self.index]
}
#[must_use]
pub const fn id(self) -> usize {
self.index
}
#[must_use]
pub fn kind(self) -> ItemKind {
self.data().kind
}
#[must_use]
pub fn span(self) -> Range<usize> {
self.data().span.clone()
}
#[must_use]
pub fn raw(self) -> &'doc str {
&self.document.source[self.span()]
}
#[must_use]
pub fn line_ending(self) -> LineEnding {
self.data().ending
}
#[must_use]
pub fn content_span(self) -> Range<usize> {
let span = self.span();
span.start..span.end - self.line_ending().as_str().len()
}
#[must_use]
pub fn entry(self) -> Option<Entry<'doc>> {
self.data().entry.map(|index| Entry {
document: self.document,
index,
})
}
#[must_use]
pub fn comment_text(self) -> Option<&'doc str> {
self.data()
.comment
.as_ref()
.map(|span| &self.document.source[span.clone()])
}
#[must_use]
pub fn diagnostic(self) -> Option<&'doc Diagnostic> {
self.data().invalid.as_ref()
}
#[must_use]
pub fn invalid_code(self) -> Option<DiagnosticCode> {
self.diagnostic().map(Diagnostic::code)
}
}