use super::python::{colon_at, final_ending, key_end};
use super::{is_space, line_breaks, skip_whitespace, valid_key_in, Parser, PhysicalLine};
use crate::{ColonSeparator, DiagnosticCode, EntryTokens, ItemKind, LineEnding, Quote};
use core::ops::Range;
pub(super) fn statement<const D: bool>(
parser: &mut Parser<'_, D>,
start: usize,
_first: PhysicalLine,
) -> usize {
let source = parser.source;
let dialect = parser.dialect();
let at = skip_whitespace(source, start, source.len(), dialect.whitespace);
let indent = start..at;
if at == source.len() {
return parser.plain_item(ItemKind::Blank, start..at, final_ending(source, at), None);
}
let first = anchored_line(parser, at);
if at == first.content_end {
let end = if first.end == at {
at + source[at..].chars().next().map_or(0, char::len_utf8)
} else {
first.end
};
return parser.plain_item(ItemKind::Blank, start..end, first.ending, None);
}
if source.as_bytes()[at] == b'#' {
return parser.plain_item(
ItemKind::Comment,
start..first.end,
first.ending,
Some(at + 1..first.content_end),
);
}
let header = header(parser, at, true).or_else(|| header(parser, at, false));
let Some(header) = header else {
return parser.invalid(
start,
first,
DiagnosticCode::InvalidKey,
at..first.content_end,
parser.line + line_breaks(&source[start..at]),
);
};
let mut tokens = EntryTokens {
indent,
export: header.export,
space_after_export: header.export_end..header.key.start,
key: header.key,
space_before_eq: header.key_end..header.delimiter,
eq: header.eq.clone(),
space_after_eq: header.delimiter..header.delimiter,
value: None,
body: None,
quoted_tail: None,
trailing_space: header.delimiter..header.delimiter,
inline_comment: None,
};
let Some(eq) = header.eq else {
let last = anchored_line(parser, header.key_end);
tokens.inline_comment =
(header.delimiter < last.content_end).then_some(header.delimiter..last.content_end);
return parser.entry(start, last, tokens, Quote::None);
};
let after_eq = eq.end;
let mut value_start = after_eq;
if !dialect.raw_values
&& source.as_bytes()[eq.start] == b':'
&& dialect.colon == ColonSeparator::Whitespace
{
value_start += source[value_start..]
.chars()
.next()
.map_or(0, char::len_utf8);
if source.as_bytes().get(value_start.wrapping_sub(1)) == Some(&b'\r')
&& source.as_bytes().get(value_start) == Some(&b'\n')
{
value_start += 1;
}
}
let mut value_line = parser.physical(value_start);
if !dialect.raw_values {
let quoted_start = skip_whitespace(source, value_start, source.len(), dialect.whitespace);
let quote = source.as_bytes().get(quoted_start).copied();
if matches!(quote, Some(b'\'' | b'"')) || (dialect.backticks && quote == Some(b'`')) {
value_start = quoted_start;
value_line = parser.physical(value_start);
} else {
value_start = skip_whitespace(
source,
value_start,
value_line.content_end,
dialect.whitespace,
);
}
}
tokens.space_after_eq = after_eq..value_start;
tokens.trailing_space = value_start..value_start;
parser.parse_value(start, value_line, tokens)
}
struct Header {
export: Option<Range<usize>>,
export_end: usize,
key: Range<usize>,
key_end: usize,
delimiter: usize,
eq: Option<Range<usize>>,
}
fn header<const D: bool>(
parser: &Parser<'_, D>,
start: usize,
allow_export: bool,
) -> Option<Header> {
let source = parser.source;
let dialect = parser.dialect();
let mut at = start;
let export = if allow_export
&& dialect.export
&& source[at..].starts_with("export")
&& source[at + 6..]
.chars()
.next()
.is_some_and(|ch| is_space(ch, dialect.whitespace))
{
let range = at..at + 6;
at += 6;
Some(range)
} else {
None
};
let export_end = at;
if export.is_some() {
at = skip_whitespace(source, at, source.len(), dialect.whitespace);
}
let key_start = at;
let key_line_end = anchored_line(parser, at).content_end;
if dialect.quoted_keys && source.as_bytes().get(at) == Some(&b'\'') {
let close = source[at + 1..].find('\'').map(|offset| at + 1 + offset)?;
if close == at + 1 {
return None;
}
at = close + 1;
} else {
at = key_end(source, at, key_line_end, dialect);
if !valid_key_in(&source[key_start..at], dialect) {
return None;
}
}
let key_end = at;
let spaced = skip_whitespace(source, at, source.len(), dialect.whitespace);
let delimiter = if source.as_bytes().get(spaced) == Some(&b'=') {
spaced
} else if colon_at(source, at, dialect) {
at
} else if dialect.colon == ColonSeparator::Any && colon_at(source, spaced, dialect) {
spaced
} else {
let end = anchored_line(parser, at).content_end;
let tail = skip_whitespace(source, at, end, dialect.whitespace);
if tail != end && !parser.bare_comment_at(tail, key_end) {
return None;
}
return Some(Header {
export,
export_end,
key: key_start..key_end,
key_end,
delimiter: tail,
eq: None,
});
};
Some(Header {
export,
export_end,
key: key_start..key_end,
key_end,
delimiter,
eq: Some(delimiter..delimiter + 1),
})
}
pub(super) fn anchored_line<const D: bool>(parser: &Parser<'_, D>, at: usize) -> PhysicalLine {
let physical = parser.physical(at);
match parser.source[at..physical.content_end].find(['\u{2028}', '\u{2029}']) {
Some(relative) => PhysicalLine {
content_end: at + relative,
end: at + relative,
ending: LineEnding::None,
},
None => physical,
}
}