use super::language::LanguageSpec;
#[derive(Debug, PartialEq)]
pub(super) enum StringKind {
Double,
Single,
TripleDouble,
TripleSingle,
}
#[derive(Debug, PartialEq)]
pub(super) enum State {
Normal,
InString(StringKind),
InBlockComment(usize), InDocAttribute(Option<StringKind>),
}
pub(super) struct StepResult {
pub advance: usize,
pub new_state: Option<State>,
pub has_code: bool,
pub has_comment: bool,
pub break_line: bool,
}
impl StepResult {
fn code(advance: usize, new_state: Option<State>) -> Self {
Self {
advance,
new_state,
has_code: true,
has_comment: false,
break_line: false,
}
}
fn comment(advance: usize, new_state: Option<State>) -> Self {
Self {
advance,
new_state,
has_code: false,
has_comment: true,
break_line: false,
}
}
fn line_comment() -> Self {
Self {
advance: 0,
new_state: None,
has_code: false,
has_comment: true,
break_line: true,
}
}
}
fn bytes_start_with(haystack: &[u8], needle: &str) -> bool {
haystack.starts_with(needle.as_bytes())
}
fn skip_pragma(bytes: &[u8], start: usize, pclose: &str) -> usize {
let len = bytes.len();
let mut i = start;
while i < len {
if bytes_start_with(&bytes[i..], pclose) {
return i + pclose.len();
}
i += 1;
}
i
}
pub(super) fn step_normal(
rest: &[u8],
spec: &LanguageSpec,
full_bytes: &[u8],
pos: usize,
) -> StepResult {
if let Some((width, kind)) = string_open(rest, spec) {
return StepResult::code(width, Some(State::InString(kind)));
}
if let Some((open, _)) = spec.doc_attribute
&& bytes_start_with(rest, open)
{
return StepResult::comment(open.len(), Some(State::InDocAttribute(None)));
}
if let Some((popen, pclose)) = spec.pragma
&& bytes_start_with(rest, popen)
{
let end = skip_pragma(full_bytes, pos + popen.len(), pclose);
return StepResult::code(end - pos, None);
}
if let Some((open, _)) = spec.block_comment
&& bytes_start_with(rest, open)
{
return StepResult::comment(open.len(), Some(State::InBlockComment(1)));
}
let is_line_comment = spec.line_comments.iter().any(|lc| {
if !bytes_start_with(rest, lc) {
return false;
}
if !spec.line_comment_not_before.is_empty()
&& let Some(&next_byte) = rest.get(lc.len())
&& spec.line_comment_not_before.as_bytes().contains(&next_byte)
{
return false;
}
true
});
if is_line_comment {
return StepResult::line_comment();
}
StepResult {
advance: 1,
new_state: None,
has_code: !rest[0].is_ascii_whitespace(),
has_comment: false,
break_line: false,
}
}
fn string_delimiter(kind: &StringKind) -> &'static [u8] {
match kind {
StringKind::TripleDouble => b"\"\"\"",
StringKind::TripleSingle => b"'''",
StringKind::Double => b"\"",
StringKind::Single => b"'",
}
}
fn string_open(rest: &[u8], spec: &LanguageSpec) -> Option<(usize, StringKind)> {
let candidates = [
(spec.triple_quote_strings, StringKind::TripleDouble),
(
spec.triple_quote_strings && spec.single_quote_strings,
StringKind::TripleSingle,
),
(true, StringKind::Double),
(spec.single_quote_strings, StringKind::Single),
];
candidates.into_iter().find_map(|(enabled, kind)| {
let delim = string_delimiter(&kind);
(enabled && rest.starts_with(delim)).then_some((delim.len(), kind))
})
}
fn string_scan(rest: &[u8], kind: &StringKind) -> (usize, bool) {
let delim = string_delimiter(kind);
if rest.starts_with(delim) {
return (delim.len(), true);
}
if delim.len() == 1 && rest[0] == b'\\' {
return (rest.len().min(2), false);
}
(1, false)
}
pub(super) fn step_in_string(rest: &[u8], kind: &StringKind) -> StepResult {
let (advance, closed) = string_scan(rest, kind);
StepResult::code(advance, closed.then_some(State::Normal))
}
pub(super) fn step_in_doc_attribute(
rest: &[u8],
spec: &LanguageSpec,
inner: &Option<StringKind>,
) -> StepResult {
if let Some(kind) = inner {
let (advance, closed) = string_scan(rest, kind);
return StepResult::comment(advance, closed.then_some(State::InDocAttribute(None)));
}
let close = spec.doc_attribute.map(|(_, c)| c).unwrap_or(")]");
if bytes_start_with(rest, close) {
return StepResult::comment(close.len(), Some(State::Normal));
}
match string_open(rest, spec) {
Some((width, kind)) => StepResult::comment(width, Some(State::InDocAttribute(Some(kind)))),
None => StepResult::comment(1, None),
}
}
pub(super) fn step_in_block_comment(rest: &[u8], spec: &LanguageSpec, depth: usize) -> StepResult {
if spec.nested_block_comments
&& let Some((open, _)) = spec.block_comment
&& bytes_start_with(rest, open)
{
return StepResult::comment(open.len(), Some(State::InBlockComment(depth + 1)));
}
if let Some((_, close)) = spec.block_comment
&& bytes_start_with(rest, close)
{
let new_state = if depth <= 1 {
State::Normal
} else {
State::InBlockComment(depth - 1)
};
return StepResult::comment(close.len(), Some(new_state));
}
StepResult::comment(1, None)
}