use super::CssParser;
use crate::ast::*;
use crate::lexer::TokenKind;
use bumpalo::Bump;
use bumpalo::collections::Vec as BumpVec;
use tsv_lang::{Error, Span};
fn skip_comments_before_comma(parser: &mut CssParser<'_, '_>) -> Result<(), Error> {
if matches!(parser.current_kind(), TokenKind::Comment)
&& parser.peek_past_boundary_whitespace()? == TokenKind::Comma
{
parser.skip_boundary_whitespace_registering_comments()?;
}
Ok(())
}
fn parse_selector_list_with<'arena>(
parser: &mut CssParser<'_, 'arena>,
parse_item: fn(&mut CssParser<'_, 'arena>) -> Result<ComplexSelector<'arena>, Error>,
) -> Result<SelectorList<'arena>, Error> {
parser.skip_boundary_whitespace()?;
let start = parser.span_pos(parser.current_start());
let mut selectors = parser.bvec();
let first = parse_item(parser)?;
let mut end = first.span.end;
selectors.push(first);
loop {
parser.skip_html_comment_markers()?;
skip_comments_before_comma(parser)?;
if !parser.check(TokenKind::Comma) {
break;
}
parser.advance()?; parser.skip_boundary_whitespace_registering_comments()?;
parser.skip_html_comment_markers()?;
let sel = parse_item(parser)?;
end = sel.span.end;
selectors.push(sel);
}
Ok(SelectorList {
selectors: selectors.into_bump_slice(),
span: Span { start, end },
})
}
pub(crate) fn parse_complex_selector_list<'arena>(
parser: &mut CssParser<'_, 'arena>,
) -> Result<SelectorList<'arena>, Error> {
parse_selector_list_with(parser, parse_complex_selector)
}
pub(crate) fn parse_forgiving_selector_list<'arena>(
parser: &mut CssParser<'_, 'arena>,
) -> Result<SelectorList<'arena>, Error> {
parser.skip_boundary_whitespace()?;
let start = parser.span_pos(parser.current_start());
let mut selectors = parser.bvec();
loop {
let selector_start = parser.base_offset() + parser.current_start();
let source_start = parser.current_start();
match parse_complex_selector(parser) {
Ok(selector) => {
selectors.push(selector);
}
Err(error) if error.kind() == tsv_lang::ErrorKind::TooDeep => return Err(error),
Err(_) => {
extract_selector_until_comma_or_end(parser, source_start)?;
let selector_end = parser.base_offset() + parser.current_start();
let invalid = create_invalid_selector(parser.arena, selector_start, selector_end);
selectors.push(invalid);
}
}
parser.skip_boundary_whitespace_registering_comments()?;
if parser.check(TokenKind::Comma) {
parser.advance()?; parser.skip_boundary_whitespace_registering_comments()?;
} else {
break;
}
}
let end = selectors.last().map_or(start, |s| s.span.end);
Ok(SelectorList {
selectors: selectors.into_bump_slice(),
span: Span { start, end },
})
}
fn create_invalid_selector<'arena>(
arena: &'arena Bump,
start: usize,
end: usize,
) -> ComplexSelector<'arena> {
let span = Span {
start: start as u32,
end: end as u32,
};
let mut simple = BumpVec::new_in(arena);
simple.push(SimpleSelector::Invalid { span });
let mut children = BumpVec::new_in(arena);
children.push(RelativeSelector {
combinator: None,
combinator_span: None,
selectors: simple.into_bump_slice(),
span,
});
ComplexSelector {
children: children.into_bump_slice(),
span,
}
}
fn extract_selector_until_comma_or_end<'a>(
parser: &mut CssParser<'a, '_>,
start_pos: usize,
) -> Result<&'a str, Error> {
let mut depth = 0;
loop {
match parser.current_kind() {
TokenKind::RightParen if depth == 0 => {
break;
}
TokenKind::Comma if depth == 0 => {
break;
}
TokenKind::LeftParen | TokenKind::LeftBracket | TokenKind::LeftBrace => {
depth += 1;
parser.advance()?;
}
TokenKind::RightParen | TokenKind::RightBracket | TokenKind::RightBrace => {
if depth > 0 {
depth -= 1;
}
parser.advance()?;
}
TokenKind::Eof => {
return Err(Error::unexpected_eof(
parser.base_offset() + parser.current_start(),
));
}
_ => {
parser.advance()?;
}
}
}
let end_pos = parser.current_start();
let raw = &parser.source()[start_pos..end_pos];
Ok(raw)
}
pub(crate) fn parse_relative_selector_list<'arena>(
parser: &mut CssParser<'_, 'arena>,
) -> Result<SelectorList<'arena>, Error> {
parse_selector_list_with(parser, parse_complex_selector)
}
pub(crate) fn parse_complex_selector<'arena>(
parser: &mut CssParser<'_, 'arena>,
) -> Result<ComplexSelector<'arena>, Error> {
parser.skip_boundary_whitespace()?;
let start = parser.span_pos(parser.current_start());
let mut children = parser.bvec();
let mut pending: Option<(Combinator, Span)> = if !pseudo_arg_terminal_nth(parser)
&& let Some(cs) = parse_explicit_combinator(parser)?
{
Some(cs)
} else {
None
};
loop {
if let Some((c, c_span)) = pending
&& !pseudo_arg_terminal_nth(parser)
&& let Some(next) = explicit_combinator_kind(parser.current_kind())
{
children.push(RelativeSelector {
combinator: Some(c),
combinator_span: Some(c_span),
selectors: &[],
span: c_span,
});
let next_span = Span {
start: parser.span_pos(parser.current_start()),
end: parser.span_pos(parser.current_end()),
};
pending = Some((next, next_span));
parser.advance()?; skip_combinator_gap(parser)?;
continue;
}
let (combinator, combinator_span) = pending.unzip();
let child = parse_relative_selector(parser, combinator, combinator_span)?;
children.push(child);
if parser.check(TokenKind::LeftBrace)
|| parser.check(TokenKind::Comma)
|| parser.check(TokenKind::RightParen)
|| parser.check(TokenKind::Eof)
{
break;
}
let Some(cs) = parse_combinator(parser)? else {
break; };
pending = Some(cs);
}
let children = children.into_bump_slice();
let end = children.last().map_or(start, |rel| rel.span.end);
Ok(ComplexSelector {
children,
span: Span { start, end },
})
}
fn explicit_combinator_kind(kind: TokenKind) -> Option<Combinator> {
match kind {
TokenKind::GreaterThan => Some(Combinator::Child),
TokenKind::Plus => Some(Combinator::NextSibling),
TokenKind::Tilde => Some(Combinator::SubsequentSibling),
TokenKind::ColumnCombinator => Some(Combinator::Column),
_ => None,
}
}
pub(crate) fn parse_explicit_combinator(
parser: &mut CssParser<'_, '_>,
) -> Result<Option<(Combinator, Span)>, Error> {
parser.skip_boundary_whitespace()?;
let combinator_start = parser.span_pos(parser.current_start());
if let Some(comb) = explicit_combinator_kind(parser.current_kind()) {
let end = parser.span_pos(parser.current_end());
parser.advance()?; skip_combinator_gap(parser)?;
Ok(Some((
comb,
Span {
start: combinator_start,
end,
},
)))
} else {
Ok(None)
}
}
pub(crate) fn parse_combinator(
parser: &mut CssParser<'_, '_>,
) -> Result<Option<(Combinator, Span)>, Error> {
let whitespace_start = parser.span_pos(parser.current_start());
parser.skip_boundary_whitespace()?;
let had_gap_comment =
matches!(parser.current_kind(), TokenKind::Comment) && comment_continues_selector(parser)?;
if had_gap_comment {
parser.skip_boundary_whitespace_registering_comments()?;
}
let combinator_start = parser.span_pos(parser.current_start());
let combinator = explicit_combinator_kind(parser.current_kind()).or_else(|| {
if (combinator_start > whitespace_start || had_gap_comment)
&& (is_selector_start(parser) || pseudo_arg_terminal_nth(parser))
{
Some(Combinator::Descendant)
} else {
None
}
});
let result = if let Some(comb) = combinator {
let (start, end) = if comb == Combinator::Descendant {
(whitespace_start, combinator_start)
} else {
(combinator_start, parser.span_pos(parser.current_end()))
};
if comb != Combinator::Descendant {
parser.advance()?; skip_combinator_gap(parser)?;
}
Some((comb, Span { start, end }))
} else {
None
};
Ok(result)
}
fn skip_combinator_gap(parser: &mut CssParser<'_, '_>) -> Result<(), Error> {
parser.skip_boundary_whitespace_registering_comments()?;
Ok(())
}
fn is_selector_start_kind(kind: TokenKind) -> bool {
matches!(
kind,
TokenKind::Identifier
| TokenKind::Dot
| TokenKind::Hash
| TokenKind::Asterisk
| TokenKind::Colon
| TokenKind::LeftBracket
| TokenKind::Ampersand
)
}
fn is_explicit_combinator_kind(kind: TokenKind) -> bool {
explicit_combinator_kind(kind).is_some()
}
fn is_selector_start(parser: &CssParser<'_, '_>) -> bool {
is_selector_start_kind(parser.current_kind())
}
fn pseudo_arg_terminal_nth(parser: &CssParser<'_, '_>) -> bool {
parser.in_pseudo_args && match_nth_value(parser.source(), parser.current_start()).is_some()
}
fn comment_continues_selector(parser: &CssParser<'_, '_>) -> Result<bool, Error> {
let after = parser.peek_past_boundary_whitespace()?;
Ok(is_selector_start_kind(after) || is_explicit_combinator_kind(after))
}
fn compound_continues_across_comments(parser: &CssParser<'_, '_>) -> Result<bool, Error> {
let (remaining, mut lexer) = parser.lookahead_lexer();
loop {
let token = lexer.next_token()?;
match token.kind {
TokenKind::Comment => continue,
TokenKind::Whitespace => return Ok(false),
TokenKind::Identifier
if crate::whitespace::boundary_prefix_len(
&remaining[token.start as usize..token.end as usize],
) > 0 =>
{
return Ok(false);
}
kind => return Ok(is_selector_start_kind(kind)),
}
}
}
fn parse_relative_selector<'arena>(
parser: &mut CssParser<'_, 'arena>,
combinator: Option<Combinator>,
combinator_span: Option<Span>,
) -> Result<RelativeSelector<'arena>, Error> {
parser.skip_boundary_whitespace()?;
let start = combinator_span.map_or_else(
|| parser.base_offset() + parser.current_start(),
|s| s.start_usize(),
);
let mut selectors = parser.bvec();
loop {
let simple = parse_simple_selector(parser)?;
selectors.push(simple);
if matches!(parser.current_kind(), TokenKind::Comment) {
if compound_continues_across_comments(parser)? {
while matches!(parser.current_kind(), TokenKind::Comment) {
parser.register_current_comment();
parser.advance()?;
}
continue;
}
break;
}
if parser.boundary_run_len() > 0 {
break;
}
if !is_simple_selector_chain(parser) {
break;
}
}
let end = parser.span_pos(parser.current_start());
if selectors.is_empty() {
return Err(parser.error_expected_at("selector", start));
}
Ok(RelativeSelector {
combinator,
combinator_span,
selectors: selectors.into_bump_slice(),
span: Span {
start: start as u32,
end,
},
})
}
fn is_simple_selector_chain(parser: &CssParser<'_, '_>) -> bool {
is_selector_start_kind(parser.current_kind())
}
pub(crate) fn parse_simple_selector<'arena>(
parser: &mut CssParser<'_, 'arena>,
) -> Result<SimpleSelector<'arena>, Error> {
let start = parser.base_offset() + parser.current_start();
if parser.in_pseudo_args
&& let Some(value_end) = match_nth_value(parser.source(), parser.current_start())
{
parser.resume_at(value_end)?;
return Ok(SimpleSelector::Nth {
span: Span {
start: start as u32,
end: parser.span_pos(value_end),
},
});
}
match parser.current_kind() {
TokenKind::Identifier => {
if matches!(parser.peek_past_comments()?, TokenKind::Pipe) {
let namespace_span = Some(Span {
start: start as u32,
end: parser.span_pos(parser.current_end()),
});
parser.advance()?; parser.register_and_skip_comments()?;
parser.advance()?; parser.register_and_skip_comments()?;
let universal = parser.check(TokenKind::Asterisk);
if !universal && !parser.check(TokenKind::Identifier) {
return Err(
parser.error_expected_after("element name or '*'", "namespace prefix")
);
}
let span = Span {
start: start as u32,
end: parser.span_pos(parser.current_end()),
};
parser.advance()?;
Ok(if universal {
SimpleSelector::Universal {
namespace_span,
span,
}
} else {
SimpleSelector::Type {
namespace_span,
span,
}
})
} else {
parser.advance()?;
let end = parser.span_pos(parser.current_start());
Ok(SimpleSelector::Type {
namespace_span: None,
span: Span {
start: start as u32,
end,
},
})
}
}
TokenKind::Dot => {
parser.advance()?; parser.register_and_skip_comments()?;
if !parser.check(TokenKind::Identifier) {
return Err(parser.error_expected_after("class name", "."));
}
let end = parser.span_pos(parser.current_end());
parser.advance()?;
Ok(SimpleSelector::Class {
span: Span {
start: start as u32,
end,
},
})
}
TokenKind::Hash => {
parser.advance()?; if !parser.check(TokenKind::Identifier) {
return Err(parser.error_expected_after("ID name", "#"));
}
let end = parser.span_pos(parser.current_end());
parser.advance()?;
Ok(SimpleSelector::Id {
span: Span {
start: start as u32,
end,
},
})
}
TokenKind::Asterisk => {
let namespaced = matches!(parser.peek_past_comments()?, TokenKind::Pipe);
let namespace_span = Span {
start: start as u32,
end: parser.span_pos(parser.current_end()),
};
parser.advance()?;
if namespaced {
parser.register_and_skip_comments()?;
}
if parser.check(TokenKind::Pipe) {
parser.advance()?; parser.register_and_skip_comments()?;
let universal = parser.check(TokenKind::Asterisk);
if !universal && !parser.check(TokenKind::Identifier) {
return Err(parser.error_expected_after(
"element name or '*'",
"universal namespace prefix",
));
}
let span = Span {
start: start as u32,
end: parser.span_pos(parser.current_end()),
};
parser.advance()?;
let namespace_span = Some(namespace_span); Ok(if universal {
SimpleSelector::Universal {
namespace_span,
span,
}
} else {
SimpleSelector::Type {
namespace_span,
span,
}
})
} else {
let end = parser.span_pos(parser.current_start());
Ok(SimpleSelector::Universal {
namespace_span: None,
span: Span {
start: start as u32,
end,
},
})
}
}
TokenKind::Colon => {
super::pseudo::parse_pseudo_selector(parser, start)
}
TokenKind::LeftBracket => {
super::attributes::parse_attribute_selector(parser, start)
}
TokenKind::Ampersand => {
let end = parser.span_pos(parser.current_end());
parser.advance()?;
Ok(SimpleSelector::Nesting {
span: Span {
start: start as u32,
end,
},
})
}
TokenKind::Percentage => {
let value_str = &parser.source()[parser.current_start()..parser.current_end() - 1];
let value = value_str.parse::<f64>().map_err(|_| {
parser.error_msg_at(&format!("Invalid percentage value: {value_str}"), start)
})?;
let end = parser.span_pos(parser.current_end());
parser.advance()?;
Ok(SimpleSelector::Percentage {
value,
span: Span {
start: start as u32,
end,
},
})
}
TokenKind::Pipe => {
let namespace_span = Some(Span {
start: start as u32,
end: start as u32,
});
parser.advance()?; parser.register_and_skip_comments()?;
if parser.check(TokenKind::Identifier) {
let end = parser.span_pos(parser.current_end());
parser.advance()?;
Ok(SimpleSelector::Type {
namespace_span, span: Span {
start: start as u32,
end,
},
})
} else if parser.check(TokenKind::Asterisk) {
let end = parser.span_pos(parser.current_end());
parser.advance()?;
Ok(SimpleSelector::Universal {
namespace_span, span: Span {
start: start as u32,
end,
},
})
} else {
Err(parser.error_expected_after("element name or '*'", "no-namespace prefix '|'"))
}
}
_ => Err(parser.error_msg_at(
&format!("Unexpected token in selector: {}", parser.current_kind()),
start,
)),
}
}
fn is_anb_ws(c: char, spec: bool) -> bool {
if spec {
crate::whitespace::is_boundary_whitespace(c)
} else {
tsv_lang::is_js_whitespace(c)
}
}
fn skip_anb_ws(source: &str, mut i: usize, spec: bool) -> usize {
let bytes = source.as_bytes();
while let Some(&b) = bytes.get(i) {
let width = if b.is_ascii() {
if !is_anb_ws(b as char, spec) {
break;
}
1
} else {
let Some(c) = source[i..].chars().next() else {
break;
};
if !is_anb_ws(c, spec) {
break;
}
c.len_utf8()
};
i += width;
}
i
}
fn skip_digits(bytes: &[u8], mut i: usize) -> usize {
while bytes.get(i).is_some_and(u8::is_ascii_digit) {
i += 1;
}
i
}
fn of_keyword_ends_at(source: &str, i: usize) -> bool {
match source[i..].chars().next() {
None => true,
Some(c) if c.is_ascii() => !(c.is_ascii_alphanumeric() || c == '-' || c == '_'),
Some(c) => is_anb_ws(c, true) || !crate::lexer::is_non_ascii_identifier_codepoint(c),
}
}
fn match_nth_value(source: &str, pos: usize) -> Option<usize> {
let bytes = source.as_bytes();
let value_end = match_an_plus_b(source, pos, false)?;
let after = skip_anb_ws(source, value_end, false);
if matches!(bytes.get(after), Some(b',' | b')')) {
return Some(value_end);
}
if after > value_end && bytes[after..].starts_with(b"of") {
let of_end = after + 2;
let after_of = skip_anb_ws(source, of_end, false);
if after_of > of_end {
return Some(after_of);
}
}
None
}
fn match_an_plus_b(source: &str, start: usize, spec: bool) -> Option<usize> {
let bytes = source.as_bytes();
if bytes[start..].starts_with(b"even") {
return Some(start + 4);
}
if bytes[start..].starts_with(b"odd") {
return Some(start + 3);
}
let sign = bytes
.get(start)
.copied()
.filter(|b| matches!(b, b'+' | b'-'));
let after_sign = start + usize::from(sign.is_some());
let after_digits = skip_digits(bytes, after_sign);
let had_digits = after_digits > after_sign;
let is_n = matches!(bytes.get(after_digits), Some(&b'n'))
|| (spec && had_digits && matches!(bytes.get(after_digits), Some(&b'N')));
if is_n {
let after_n = after_digits + 1; let plus_only = !spec && sign == Some(b'-');
match match_anb_tail(source, after_n, plus_only, spec) {
Some(end) => Some(end),
None => (spec || sign != Some(b'-')).then_some(after_n),
}
} else if had_digits && (spec || sign != Some(b'-')) {
Some(after_digits)
} else {
None
}
}
fn skip_anb_ws_and_comments(source: &str, mut i: usize) -> usize {
let bytes = source.as_bytes();
loop {
i = skip_anb_ws(source, i, true);
if !crate::comments::is_comment_start(bytes, i) {
return i;
}
i = crate::comments::comment_end(bytes, i);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum NthTerminator {
Paren(usize),
Of(usize),
}
pub(crate) fn nth_arg_terminator(source: &str, pos: usize) -> Option<NthTerminator> {
let bytes = source.as_bytes();
let value_end = match_an_plus_b(source, pos, true)?;
let after = skip_anb_ws_and_comments(source, value_end);
match bytes.get(after) {
Some(b')') => Some(NthTerminator::Paren(after)),
_ => (after > value_end
&& bytes[after..].starts_with(b"of")
&& of_keyword_ends_at(source, after + 2))
.then_some(NthTerminator::Of(after)),
}
}
fn match_anb_tail(source: &str, pos: usize, plus_only: bool, spec: bool) -> Option<usize> {
let bytes = source.as_bytes();
let skip_trivia: fn(&str, usize) -> usize = if spec {
skip_anb_ws_and_comments
} else {
|source, i| skip_anb_ws(source, i, false)
};
let op_pos = skip_trivia(source, pos);
let op = *bytes.get(op_pos)?;
if op != b'+' && (plus_only || op != b'-') {
return None;
}
let digits_start = skip_trivia(source, op_pos + 1);
let end = skip_digits(bytes, digits_start);
(end > digits_start).then_some(end)
}
#[cfg(test)]
mod tests {
use super::{
Combinator, NthTerminator, TokenKind, explicit_combinator_kind,
is_explicit_combinator_kind, match_nth_value, nth_arg_terminator,
};
#[test]
fn explicit_combinator_kind_maps_the_four_symbols() {
assert_eq!(
explicit_combinator_kind(TokenKind::GreaterThan),
Some(Combinator::Child)
);
assert_eq!(
explicit_combinator_kind(TokenKind::Plus),
Some(Combinator::NextSibling)
);
assert_eq!(
explicit_combinator_kind(TokenKind::Tilde),
Some(Combinator::SubsequentSibling)
);
assert_eq!(
explicit_combinator_kind(TokenKind::ColumnCombinator),
Some(Combinator::Column)
);
for kind in [
TokenKind::Identifier,
TokenKind::Dot,
TokenKind::Comma,
TokenKind::Whitespace,
TokenKind::LeftBrace,
] {
assert_eq!(explicit_combinator_kind(kind), None);
}
for kind in [
TokenKind::GreaterThan,
TokenKind::Plus,
TokenKind::Tilde,
TokenKind::ColumnCombinator,
TokenKind::Identifier,
TokenKind::Comma,
] {
assert_eq!(
is_explicit_combinator_kind(kind),
explicit_combinator_kind(kind).is_some()
);
}
}
#[test]
fn nth_arg_terminator_spec_grammar() {
for input in [
"2n)",
"odd)",
"even)",
"0)",
"-3)", "-0)",
"-2n)", "-n)", "-2n-3)", "-n-3)", "-2n - 3)", "-n - 3)", "2N)", "2N+1)",
"2n of .x)", "2n of.x)", "-n + 3 of li.a)", "2n /* c */)", "3 /* c */ of .x)",
"2n /* c */ + 1)",
"2n + /* c */ 1)",
"2n/* c */+1)", "2n /* a */ /* b */ + 1)", "2n/* a *//* b */+1)", "-2n /* c */ - 3)", "n /* c */ + 1)", "2n- /* c */ 1)", "2n /* c */ + 1 of .x)", "2n/* c */of .x)", "2n\u{a0})",
"2n\u{feff})",
"2n\u{85})",
"even\u{a0})",
"2n\u{a0}+\u{a0}1)",
"2n\u{feff}+ 1)",
"2n +\u{feff}1)",
"2n\u{a0}of .x)",
"2n of\u{a0}.x)", "2n of\u{a0}x)",
] {
assert!(
nth_arg_terminator(input, 0).is_some(),
"expected {input:?} to take the Nth path"
);
}
assert_eq!(
nth_arg_terminator("2n\u{a0})", 0),
Some(NthTerminator::Paren(4))
);
assert_eq!(
nth_arg_terminator("2n\u{a0}of .x)", 0),
Some(NthTerminator::Of(4))
);
assert_eq!(
nth_arg_terminator("2n /* c */ of .x)", 0),
Some(NthTerminator::Of(11))
);
for input in [
".a)", "div)", "#id)", "N)", "EVEN)", "even odd)", "2n .foo)", "2n, .a)", "2n OF .x)", "2n offset)", ")", "2/* c */n)",
"2n /* c + 1)",
"2n of\u{2665}x)",
] {
assert!(
nth_arg_terminator(input, 0).is_none(),
"expected {input:?} to take the selector-list path"
);
}
}
#[test]
fn nth_value_matches_svelte_regex() {
for (input, value) in [
("2n)", "2n"),
("2n+1)", "2n+1"),
("2n + 1)", "2n + 1"),
("2n - 1)", "2n - 1"),
("2n\x0b+\x0b1)", "2n\x0b+\x0b1"),
("2n\u{a0}+\u{a0}1)", "2n\u{a0}+\u{a0}1"),
("2n\u{feff})", "2n"),
("2n\u{a0},", "2n"),
("2n\u{a0}of\u{a0}.x)", "2n\u{a0}of\u{a0}"),
("2n of\u{feff}.x)", "2n of\u{feff}"),
("0)", "0"),
("123)", "123"),
("+3)", "+3"),
("+2n)", "+2n"),
("+2n+1)", "+2n+1"),
("n)", "n"),
("odd)", "odd"),
("even)", "even"),
("-n+3)", "-n+3"),
("-2n+1)", "-2n+1"),
("-n + 3)", "-n + 3"),
("2n )", "2n"),
("2n,", "2n"),
("odd ,", "odd"),
("2n of .x)", "2n of "),
("odd of .a, .b)", "odd of "),
("-n + 3 of .a .b)", "-n + 3 of "),
("2n of .x)", "2n of "),
] {
assert_eq!(
match_nth_value(input, 0),
Some(value.len()),
"expected {input:?} to match {value:?}"
);
}
for input in [
"-n)", "-2n)", "-1)", "nth)", "evens)", "div)", ".a)", "2n .foo)", "+)", "2nx)", "2n of.x)", "2nof .x)", "2n\u{85})",
"2n\u{85}+ 1)",
"2n often )", "2n /* c */ + 1)",
"2n + /* c */ 1)",
"2n/* c */+1)",
"2n /* c */)",
] {
assert_eq!(
match_nth_value(input, 0),
None,
"expected {input:?} to not match an An+B"
);
}
}
}