use super::{CssParser, is_boolean_operator_keyword};
use crate::ast::*;
use crate::lexer::TokenKind;
use crate::parser::selectors::{parse_complex_selector_list, parse_forgiving_selector_list};
use crate::whitespace::{Comments, Edge, holds_boundary_member, spell_run};
use tsv_lang::{Error, Span};
fn is_function_token(parser: &CssParser<'_, '_>) -> bool {
parser.check(TokenKind::Identifier) && {
let end_pos = parser.current_end();
parser.source.get(end_pos..=end_pos) == Some("(")
}
}
fn at_selector_function(parser: &CssParser<'_, '_>) -> bool {
is_function_token(parser) && parser.current_identifier().eq_ignore_ascii_case("selector")
}
fn parse_selector_argument<'arena>(
parser: &mut CssParser<'_, 'arena>,
arg_start: usize,
comments_len: usize,
) -> Result<Option<SelectorList<'arena>>, Error> {
match parse_complex_selector_list(parser) {
Ok(list) if parser.check(TokenKind::RightParen) => return Ok(Some(list)),
Err(error) if error.kind() == tsv_lang::ErrorKind::TooDeep => return Err(error),
Ok(_) | Err(_) => {}
}
parser.rewind_to(arg_start, comments_len)?;
Ok(None)
}
fn needs_separator_before(prev: Option<TokenKind>, trailing_spaces: usize) -> bool {
trailing_spaces == 0 && !matches!(prev, None | Some(TokenKind::LeftParen))
}
fn skip_gap_registering_comments(
parser: &mut CssParser<'_, '_>,
end: &mut usize,
run: &mut OperatorRun,
) -> Result<(), Error> {
let gap_start = parser.current_start();
let mark = run.text.len();
parser.skip_boundary_whitespace()?;
while parser.check(TokenKind::Comment) {
parser.register_current_comment();
run.push_comment(
parser.current_value(),
parser.span_pos(parser.current_end()),
);
*end = parser.base_offset() + parser.current_end();
parser.advance()?;
parser.skip_boundary_whitespace()?;
}
let gap_end = parser.current_start();
if holds_boundary_member(parser.source(), gap_start, gap_end) {
let spelled = spell_run(
parser.source(),
gap_start,
gap_end,
false,
(Edge::Presence, Edge::Presence),
Comments::Carried,
);
run.text.truncate(mark);
run.push_gap(&spelled, parser.span_pos(gap_end));
}
Ok(())
}
struct OperatorRun {
text: String,
operators: Option<OperatorBounds>,
comments_len: usize,
source_end: u32,
glued_to_next: bool,
}
struct OperatorBounds {
start: usize,
end: usize,
comments_len: usize,
source: Span,
}
impl OperatorRun {
fn new(comments_len: usize) -> Self {
Self {
text: String::new(),
operators: None,
comments_len,
source_end: 0,
glued_to_next: false,
}
}
fn push_comment(&mut self, comment: &str, end: u32) {
self.push_word(comment);
self.source_end = end;
}
fn push_gap(&mut self, spelled: &str, end: u32) {
let spelled = if self.text.is_empty() {
spelled.trim_start_matches(' ')
} else {
spelled
};
let (body, glued) = match spelled.strip_suffix(' ') {
Some(body) => (body, false),
None => (spelled, true),
};
self.text.push_str(body);
self.glued_to_next = glued;
self.source_end = end;
}
#[must_use]
fn push_operator(
&mut self,
operator: &str,
comments_len: usize,
source: Span,
) -> Option<usize> {
let unregister_to = self.operators.as_ref().map(|bounds| bounds.comments_len);
let start = self.push_word(operator);
self.source_end = source.end;
match &mut self.operators {
Some(bounds) => {
bounds.end = self.text.len();
bounds.source.end = source.end;
}
None => {
self.operators = Some(OperatorBounds {
start,
end: self.text.len(),
comments_len,
source,
});
}
}
unregister_to
}
fn push_word(&mut self, word: &str) -> usize {
if !self.text.is_empty() && !self.glued_to_next {
self.text.push(' ');
}
self.glued_to_next = false;
let start = self.text.len();
self.text.push_str(word);
start
}
fn connector_run(&self) -> Option<(&str, Span)> {
self.operators
.as_ref()
.map(|bounds| (&self.text[bounds.start..bounds.end], bounds.source))
}
fn bind(&mut self, comments_len: usize) {
self.text.clear();
self.operators = None;
self.comments_len = comments_len;
self.glued_to_next = false;
}
fn finish(&self) -> Option<(&str, usize, u32)> {
self.operators
.as_ref()
.map(|_| (self.text.as_str(), self.comments_len, self.source_end))
}
}
fn boolean_operator_position(prev: Option<TokenKind>, in_selector_args: bool) -> bool {
!in_selector_args
&& matches!(
prev,
None | Some(TokenKind::LeftParen) | Some(TokenKind::RightParen)
)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum ConditionReader {
Value,
Raw,
}
pub(super) fn parse_condition_query<'arena>(
parser: &mut CssParser<'_, 'arena>,
reader: ConditionReader,
) -> Result<(ConditionQuery<'arena>, Span), Error> {
parser.skip_boundary_whitespace()?;
let start = parser.base_offset() + parser.current_start();
let mut parts = parser.bvec();
let mut current_connector: Option<ConditionConnector> = None;
let mut end_pos = start;
let mut run = OperatorRun::new(parser.comments.len());
while !parser.at_prelude_end() {
skip_gap_registering_comments(parser, &mut end_pos, &mut run)?;
if parser.check(TokenKind::Identifier) {
let ident = parser.current_identifier();
let connector = if ident.eq_ignore_ascii_case("and") {
Some(ConditionConnector::And)
} else if ident.eq_ignore_ascii_case("or") {
Some(ConditionConnector::Or)
} else {
None
};
if let Some(conn) = connector {
current_connector = Some(conn);
let source = Span {
start: parser.span_pos(parser.current_start()),
end: parser.span_pos(parser.current_end()),
};
if let Some(comments_len) =
run.push_operator(parser.current_value(), parser.comments.len(), source)
{
parser.comments.truncate(comments_len);
}
end_pos = parser.base_offset() + parser.current_end();
parser.advance()?;
skip_gap_registering_comments(parser, &mut end_pos, &mut run)?;
continue;
}
}
let connector =
current_connector
.take()
.zip(run.connector_run())
.map(|(kind, (text, span))| ConnectorRun {
kind,
text: parser.alloc_str_in(text),
span,
});
let part_start = parser.span_pos(parser.current_start());
match parse_condition_part(parser, connector, end_pos, reader)? {
ConditionPartOutcome::Parsed { part, end, .. } => {
end_pos = end;
run.bind(parser.comments.len());
if let Some(part) = part {
parts.push(part);
}
}
ConditionPartOutcome::NotAPart { consumed, end } => {
if let Some(consumed) = consumed {
let source = Span {
start: part_start,
end: end as u32,
};
if let Some(comments_len) =
run.push_operator(consumed, parser.comments.len(), source)
{
parser.comments.truncate(comments_len);
}
}
end_pos = end;
break;
}
}
}
let trailing_operators = run.finish().map(|(text, comments_len, source_end)| {
parser.comments.truncate(comments_len);
end_pos = end_pos.max(source_end as usize);
parser.alloc_str_in(text)
});
let span = Span {
start: start as u32,
end: end_pos as u32,
};
Ok((
ConditionQuery {
parts: parts.into_bump_slice(),
trailing_operators,
},
span,
))
}
enum ConditionPartOutcome<'arena> {
Parsed {
part: Option<ConditionPart<'arena>>,
end: usize,
closed: bool,
},
NotAPart {
consumed: Option<&'arena str>,
end: usize,
},
}
fn parse_condition_part<'arena>(
parser: &mut CssParser<'_, 'arena>,
connector: Option<ConnectorRun<'arena>>,
mut end_pos: usize,
reader: ConditionReader,
) -> Result<ConditionPartOutcome<'arena>, Error> {
let part_start = parser.span_pos(parser.current_start());
let mut part_buf = String::new();
let mut segments = parser.bvec();
let mut trailing_spaces: usize = 0;
let mut paren_depth: usize = 0;
let mut general_enclosed_selector: Option<usize> = None;
if parser.check(TokenKind::Identifier) {
let ident = parser.current_identifier();
if ident.eq_ignore_ascii_case("not") {
part_buf.push_str(parser.current_value());
end_pos = parser.base_offset() + parser.current_end();
parser.advance()?;
let gap_start = parser.current_start();
let not_end = part_buf.len();
parser.skip_boundary_whitespace()?;
while parser.check(TokenKind::Comment) {
part_buf.push(' ');
part_buf.push_str(parser.current_value());
end_pos = parser.base_offset() + parser.current_end();
parser.advance()?;
parser.skip_boundary_whitespace()?;
}
let gap_end = parser.current_start();
if holds_boundary_member(parser.source(), gap_start, gap_end) {
part_buf.truncate(not_end);
part_buf.push_str(&spell_run(
parser.source(),
gap_start,
gap_end,
false,
(Edge::Space, Edge::Presence),
Comments::Carried,
));
trailing_spaces = usize::from(part_buf.ends_with(' '));
} else {
part_buf.push(' ');
trailing_spaces = 1;
}
}
}
let mut at_selector_args = false;
if is_function_token(parser) {
at_selector_args = at_selector_function(parser);
part_buf.push_str(parser.current_value());
trailing_spaces = 0;
parser.advance()?;
}
if !parser.check(TokenKind::LeftParen) {
let consumed = (!part_buf.is_empty()).then(|| {
parser
.alloc_str_in(part_buf.trim_end_matches(crate::whitespace::is_boundary_whitespace))
});
return Ok(ConditionPartOutcome::NotAPart {
consumed,
end: end_pos,
});
}
let mut closed = false;
let mut prev_token_kind: Option<TokenKind> = None;
let mut last_significant_kind: Option<TokenKind> = None;
while !parser.check(TokenKind::Eof) {
let opening_selector_args = at_selector_args;
at_selector_args = false;
if opening_selector_args && parser.check(TokenKind::LeftParen) {
part_buf.push('(');
trailing_spaces = 0;
let paren_start = parser.span_pos(parser.current_start());
parser.advance()?; let arg_start = parser.current_start();
let comments_len = parser.comments.len();
if let Some(list) = parse_selector_argument(parser, arg_start, comments_len)? {
if !part_buf.is_empty() {
segments.push(ConditionSegment::Text(parser.alloc_str_in(&part_buf)));
part_buf.clear();
}
let paren = Span {
start: paren_start,
end: parser.span_pos(parser.current_end()),
};
segments.push(ConditionSegment::Selectors { list, paren });
part_buf.push(')');
end_pos = parser.base_offset() + parser.current_end();
parser.advance()?;
prev_token_kind = Some(TokenKind::RightParen);
last_significant_kind = Some(TokenKind::RightParen);
if paren_depth == 0 {
closed = true;
break;
}
continue;
}
paren_depth += 1;
general_enclosed_selector.get_or_insert(paren_depth);
prev_token_kind = Some(TokenKind::LeftParen);
last_significant_kind = Some(TokenKind::LeftParen);
continue;
}
if parser.check(TokenKind::LeftParen) {
paren_depth += 1;
} else if parser.check(TokenKind::RightParen) {
if paren_depth == 0 {
break;
}
paren_depth -= 1;
if general_enclosed_selector == Some(paren_depth + 1) {
general_enclosed_selector = None;
}
}
if paren_depth == 0 && parser.check(TokenKind::RightParen) {
part_buf.push(')');
end_pos = parser.base_offset() + parser.current_end();
parser.advance()?;
closed = true;
break;
}
if parser.check(TokenKind::Whitespace) {
let after_value_colon = matches!(prev_token_kind, Some(TokenKind::Colon));
let skip_whitespace = matches!(prev_token_kind, Some(TokenKind::LeftParen))
|| (matches!(parser.peek_kind(), Ok(TokenKind::RightParen)) && !after_value_colon);
parser.advance()?;
if skip_whitespace {
continue;
}
part_buf.push(' ');
trailing_spaces += 1;
prev_token_kind = Some(TokenKind::Whitespace);
continue;
}
let is_comment = matches!(parser.current_kind(), TokenKind::Comment);
let is_bool_op = matches!(parser.current_kind(), TokenKind::Identifier)
&& is_boolean_operator_keyword(parser.current_identifier())
&& boolean_operator_position(
last_significant_kind,
general_enclosed_selector.is_some(),
);
at_selector_args = at_selector_function(parser);
let pads_comment = is_comment && general_enclosed_selector.is_none();
let pads_before = is_bool_op || pads_comment;
if pads_before && needs_separator_before(prev_token_kind, trailing_spaces) {
part_buf.push(' ');
trailing_spaces += 1;
}
let pads_comma = matches!(parser.current_kind(), TokenKind::Comma)
&& reader == ConditionReader::Value
&& general_enclosed_selector.is_none();
let strips_leading_space = pads_comma
|| (matches!(parser.current_kind(), TokenKind::Colon)
&& general_enclosed_selector.is_none());
if strips_leading_space {
part_buf.truncate(part_buf.len() - trailing_spaces);
}
match parser.current_kind() {
TokenKind::String { quote } => {
let content =
&parser.source()[parser.current_start() + 1..parser.current_end() - 1];
part_buf.push(quote);
part_buf.push_str(content);
part_buf.push(quote);
}
_ => part_buf.push_str(parser.current_value()),
}
trailing_spaces = 0;
let current_kind = parser.current_kind();
end_pos = parser.base_offset() + parser.current_end();
parser.advance()?;
if is_bool_op
&& !parser.check(TokenKind::Whitespace)
&& !parser.check(TokenKind::RightParen)
{
part_buf.push(' ');
trailing_spaces += 1;
}
if pads_comment
&& !parser.check(TokenKind::Whitespace)
&& !parser.check(TokenKind::RightParen)
{
part_buf.push(' ');
trailing_spaces += 1;
}
if pads_comma
&& !parser.check(TokenKind::Whitespace)
&& !parser.check(TokenKind::RightParen)
{
part_buf.push(' ');
trailing_spaces += 1;
}
if !parser.check(TokenKind::Whitespace)
&& matches!(current_kind, TokenKind::Colon)
&& general_enclosed_selector.is_none()
&& matches!(
last_significant_kind,
Some(TokenKind::Identifier)
| Some(TokenKind::Number)
| Some(TokenKind::Dimension { .. })
| Some(TokenKind::Percentage)
)
{
part_buf.push(' ');
trailing_spaces += 1;
}
prev_token_kind = Some(current_kind);
if !matches!(current_kind, TokenKind::Whitespace | TokenKind::Comment) {
last_significant_kind = Some(current_kind);
}
}
if !part_buf.is_empty() {
segments.push(ConditionSegment::Text(parser.alloc_str_in(&part_buf)));
}
if let Some(ConditionSegment::Text(first)) = segments.first_mut() {
*first = first.trim_start_matches(crate::whitespace::is_boundary_whitespace);
}
if let Some(ConditionSegment::Text(last)) = segments.last_mut() {
*last = last.trim_end_matches(crate::whitespace::is_boundary_whitespace);
}
let is_empty = segments
.iter()
.all(|segment| matches!(segment, ConditionSegment::Text(text) if text.is_empty()));
let part = (!is_empty).then(|| ConditionPart {
connector,
segments: segments.into_bump_slice(),
span: Span {
start: part_start,
end: end_pos as u32,
},
});
Ok(ConditionPartOutcome::Parsed {
part,
end: end_pos,
closed,
})
}
pub(super) fn parse_supports_function_condition<'arena>(
parser: &mut CssParser<'_, 'arena>,
) -> Result<Option<(ConditionQuery<'arena>, Span)>, Error> {
let start = parser.base_offset() + parser.current_start();
let ConditionPartOutcome::Parsed {
part,
end,
closed: true,
} = parse_condition_part(parser, None, start, ConditionReader::Value)?
else {
return Ok(None);
};
let mut parts = parser.bvec();
if let Some(part) = part {
parts.push(part);
}
Ok(Some((
ConditionQuery {
parts: parts.into_bump_slice(),
trailing_operators: None,
},
Span {
start: start as u32,
end: end as u32,
},
)))
}
pub(super) fn parse_container_prelude<'arena>(
parser: &mut CssParser<'_, 'arena>,
) -> Result<(Option<&'arena str>, ConditionQuery<'arena>, Span), Error> {
parser.skip_boundary_whitespace()?;
let start = parser.span_pos(parser.current_start());
let container_name = if parser.check(TokenKind::Identifier)
&& !is_function_token(parser)
&& !is_boolean_operator_keyword(parser.current_identifier())
{
let name = parser.alloc_str_in(parser.current_value());
parser.advance()?;
parser.skip_boundary_whitespace()?;
Some(name)
} else {
None
};
let (condition, cond_span) = parse_condition_query(parser, ConditionReader::Raw)?;
let span = Span {
start,
end: cond_span.end,
};
Ok((container_name, condition, span))
}
fn parse_scope_clause<'arena>(
parser: &mut CssParser<'_, 'arena>,
what: &str,
) -> Result<ScopeClause<'arena>, Error> {
let paren_start = parser.span_pos(parser.current_start());
parser.advance()?; parser.skip_boundary_whitespace_registering_comments()?; let list = parse_forgiving_selector_list(parser)?;
parser.skip_boundary_whitespace_registering_comments()?; if !parser.check(TokenKind::RightParen) {
return Err(parser.error_expected_after("')'", what));
}
let paren_end = parser.span_pos(parser.current_end());
parser.advance()?; Ok(ScopeClause {
list,
paren: Span {
start: paren_start,
end: paren_end,
},
})
}
pub(super) fn parse_scope_prelude<'arena>(
parser: &mut CssParser<'_, 'arena>,
) -> Result<PreludeValue<'arena>, Error> {
parser.skip_boundary_whitespace_registering_comments()?;
let start = parser.span_pos(parser.current_start());
let mut end = start;
let root = if parser.check(TokenKind::LeftParen) {
let clause = parse_scope_clause(parser, "@scope root selectors")?;
end = clause.paren.end;
parser.skip_boundary_whitespace_registering_comments()?; Some(clause)
} else {
None
};
let limit = if let Some(to_len) = scope_to_keyword_len(parser) {
let to_start = parser.current_start();
let to_span = Span {
start: parser.span_pos(to_start),
end: parser.span_pos(to_start + to_len),
};
parser.advance_from(to_start + to_len)?;
parser.skip_boundary_whitespace_registering_comments()?; if !parser.check(TokenKind::LeftParen) {
return Err(parser.error_expected_after("'('", "'to' in @scope prelude"));
}
let clause = parse_scope_clause(parser, "@scope limit selectors")?;
end = clause.paren.end;
parser.skip_boundary_whitespace_registering_comments()?; Some(ScopeLimit { to_span, clause })
} else {
None
};
Ok(PreludeValue::Selectors {
root,
limit,
span: Span { start, end },
})
}
fn scope_to_keyword_len(parser: &CssParser<'_, '_>) -> Option<usize> {
if !parser.check(TokenKind::Identifier) {
return None;
}
if parser.current_identifier().eq_ignore_ascii_case("to") {
return Some(parser.current_end() - parser.current_start());
}
let text = &parser.source()[parser.current_start()..parser.current_end()];
let split = crate::whitespace::boundary_split_offset(text)?;
text[..split].eq_ignore_ascii_case("to").then_some(split)
}
pub(super) fn parse_custom_selector_prelude<'arena>(
parser: &mut CssParser<'_, 'arena>,
) -> Result<PreludeValue<'arena>, Error> {
let prelude_start_raw = parser.current_start();
let comments_len = parser.comments.len();
let span_start = parser.span_pos(prelude_start_raw);
if let Some((name, list)) = parse_custom_selector_head_and_list(parser)? {
parser.skip_whitespace_registering_comments()?;
if parser.at_prelude_end() {
let span = Span {
start: span_start,
end: parser.span_pos(parser.current_start()),
};
return Ok(PreludeValue::CustomSelector { name, list, span });
}
}
parser.rewind_to(prelude_start_raw, comments_len)?;
let (content, span) = super::raw::parse_raw_prelude_content(parser, false, false)?;
Ok(PreludeValue::Raw { content, span })
}
fn parse_custom_selector_head_and_list<'arena>(
parser: &mut CssParser<'_, 'arena>,
) -> Result<Option<(Span, SelectorList<'arena>)>, Error> {
parser.skip_boundary_whitespace_registering_comments()?;
if !parser.check(TokenKind::Colon) {
return Ok(None);
}
let name_start = parser.span_pos(parser.current_start());
let colon_end = parser.current_end();
parser.advance()?;
if !parser.check(TokenKind::Identifier)
|| parser.current_start() != colon_end
|| !parser.current_identifier().starts_with("--")
{
return Ok(None);
}
let name = Span {
start: name_start,
end: parser.span_pos(parser.current_end()),
};
parser.advance()?;
let before = parser.comments.len();
let saw_whitespace = parser.skip_boundary_whitespace_registering_comments()?;
if !saw_whitespace && parser.comments.len() == before {
return Ok(None);
}
if parser.at_prelude_end() {
return Ok(None);
}
match parse_complex_selector_list(parser) {
Ok(list) => Ok(Some((name, list))),
Err(error) if error.kind() == tsv_lang::ErrorKind::TooDeep => Err(error),
Err(_) => Ok(None),
}
}
pub(super) fn parse_import_prelude<'arena>(
parser: &mut CssParser<'_, 'arena>,
) -> Result<PreludeValue<'arena>, Error> {
let prelude_start_raw = parser.current_start();
let start = parser.span_pos(parser.current_start());
let mut values = parser.bvec();
parser.skip_boundary_whitespace_registering_comments()?;
if parser.at_prelude_end() {
return super::reconsume_prelude_as_raw(parser, prelude_start_raw);
}
if let TokenKind::String { .. } = parser.current_kind() {
let value_start = parser.span_pos(parser.current_start());
let value_end = parser.span_pos(parser.current_end());
values.push(CssValue::String {
content: StringCooked::Verbatim,
span: Span {
start: value_start,
end: value_end,
},
});
parser.advance()?;
} else if matches!(parser.current_kind(), TokenKind::Url)
&& !url_token_has_unclosed_paren(
&parser.source()[parser.current_start()..parser.current_end()],
)
{
let value_start = parser.span_pos(parser.current_start());
let value_end = parser.span_pos(parser.current_end());
let name_len = parser
.current_value()
.bytes()
.position(|b| b == b'(')
.unwrap_or(0);
debug_assert!(
name_len > 0,
"a url-token with no `(`: {:?}",
parser.current_value()
);
values.push(CssValue::Function {
name_span: Span {
start: value_start,
end: value_start + name_len as u32,
},
args: &[],
span: Span {
start: value_start,
end: value_end,
},
});
parser.advance()?;
}
parser.skip_boundary_whitespace_registering_comments()?;
while !parser.at_prelude_end() {
if is_function_token(parser) {
values.push(parse_function_value(parser)?);
} else if parser.check(TokenKind::Identifier) && parser.current_identifier() == "layer" {
let value_start = parser.span_pos(parser.current_start());
let value_end = parser.span_pos(parser.current_end());
values.push(CssValue::Identifier {
span: Span {
start: value_start,
end: value_end,
},
});
parser.advance()?;
} else {
break;
}
parser.skip_boundary_whitespace_registering_comments()?;
}
if !parser.at_prelude_end() {
let tail_local_start = parser.current_start();
let tail_start = parser.span_pos(tail_local_start);
let mut tail_local_end = parser.current_end();
while !parser.at_prelude_end() {
if !parser.check(TokenKind::Whitespace) {
tail_local_end = parser.current_end();
}
parser.advance()?;
}
values.push(CssValue::Identifier {
span: Span {
start: tail_start,
end: parser.span_pos(tail_local_end),
},
});
}
let end = values.last().map_or(start, |v| v.span().end);
Ok(PreludeValue::Values {
values: values.into_bump_slice(),
span: Span { start, end },
})
}
fn url_token_has_unclosed_paren(text: &str) -> bool {
let mut depth: u32 = 0;
let mut chars = text.chars();
while let Some(c) = chars.next() {
match c {
'\\' => {
chars.next(); }
'(' => depth += 1,
')' => depth = depth.saturating_sub(1),
_ => {}
}
}
depth > 0
}
fn take_comment_run(parser: &mut CssParser<'_, '_>) -> Result<Option<Span>, Error> {
let mut start = None;
let mut end = 0u32;
loop {
if parser.check(TokenKind::Whitespace) {
parser.advance()?;
} else if parser.check(TokenKind::Comment) {
start.get_or_insert_with(|| parser.span_pos(parser.current_start()));
end = parser.span_pos(parser.current_end());
parser.advance()?;
} else {
break;
}
}
Ok(start.map(|start| Span { start, end }))
}
fn consume_function_args(parser: &mut CssParser<'_, '_>) -> Result<Option<Span>, Error> {
let mut start = None;
let mut end = 0u32;
let mut depth: u32 = 0;
while !parser.check(TokenKind::Eof) {
match parser.current_kind() {
TokenKind::RightParen if depth == 0 => break,
TokenKind::LeftParen => depth += 1,
TokenKind::RightParen => depth -= 1,
_ => {}
}
if !parser.check(TokenKind::Whitespace) {
start.get_or_insert_with(|| parser.span_pos(parser.current_start()));
end = parser.span_pos(parser.current_end());
}
parser.advance()?;
}
Ok(start.map(|start| Span { start, end }))
}
fn parse_function_value<'arena>(
parser: &mut CssParser<'_, 'arena>,
) -> Result<CssValue<'arena>, Error> {
let value_start = parser.span_pos(parser.current_start());
let name = if parser.check(TokenKind::Identifier) {
parser.current_identifier_in_arena()
} else {
return Err(parser.error_expected("function name"));
};
let name_span = Span {
start: parser.span_pos(parser.current_start()),
end: parser.span_pos(parser.current_end()),
};
let name_verbatim = parser.alloc_str_in(parser.current_value());
parser.advance()?;
if !parser.check(TokenKind::LeftParen) {
return Err(parser.error_expected_after("'('", "function name"));
}
if name.eq_ignore_ascii_case("supports") {
let Some((condition, condition_span)) = parse_supports_function_condition(parser)? else {
return Err(parser.error_expected("')' to close function"));
};
return Ok(CssValue::SupportsCondition {
name: name_verbatim,
condition,
span: Span {
start: value_start,
end: condition_span.end,
},
});
}
parser.advance()?;
let mut args = parser.bvec();
if name.eq_ignore_ascii_case("url") {
parser.skip_whitespace()?;
if let TokenKind::String { .. } = parser.current_kind() {
let arg_start = parser.span_pos(parser.current_start());
let arg_end = parser.span_pos(parser.current_end());
args.push(CssValue::String {
content: StringCooked::Verbatim,
span: Span {
start: arg_start,
end: arg_end,
},
});
parser.advance()?;
args.extend(take_comment_run(parser)?.map(|span| CssValue::Identifier { span }));
} else {
while !parser.check(TokenKind::RightParen) && !parser.check(TokenKind::Eof) {
parser.advance()?;
}
}
} else if name.eq_ignore_ascii_case("layer") {
args.extend(consume_function_args(parser)?.map(|span| CssValue::Identifier { span }));
} else {
consume_function_args(parser)?;
}
if !parser.check(TokenKind::RightParen) {
return Err(parser.error_expected("')' to close function"));
}
let value_end = parser.span_pos(parser.current_end());
parser.advance()?;
Ok(CssValue::Function {
name_span,
args: args.into_bump_slice(),
span: Span {
start: value_start,
end: value_end,
},
})
}