use super::{Parser, state::ParserState};
use crate::{
Parse, Syntax,
ast::*,
error::{Error, ErrorKind, PResult},
pos::Span,
tokenizer::Token,
};
mod color_profile;
mod container;
mod counter_style;
mod custom_media;
mod custom_selector;
mod document;
mod font_feature_values;
mod import;
mod keyframes;
mod layer;
mod media;
mod namespace;
mod page;
mod scope;
mod supports;
impl<'a> Parse<'a> for AtRule<'a> {
fn parse(input: &mut Parser<'a>) -> PResult<Self> {
let (at_keyword, at_keyword_span) = input.cursor.expect_at_keyword()?;
let at_rule_name = at_keyword.ident.name();
let (prelude, block, end) = if at_rule_name.eq_ignore_ascii_case("media") {
let prelude = match input
.try_parse_full_prelude(|p| MediaQueryList::parse(p).map(AtRulePrelude::Media))
{
Ok(prelude) => Some(prelude),
Err(_)
if matches!(
input.cursor.peek()?.token,
Token::LBrace(..) | Token::Indent(..)
) =>
{
None
}
Err(_) => {
let raw = if input.syntax != Syntax::Css {
input.try_parse(|p| {
let raw = p.parse_raw_at_rule_prelude()?;
let has_substitution = matches!(
&raw,
UnknownAtRulePrelude::TokenSeq(seq)
if seq.tokens.iter().any(|t| match &t.token {
Token::HashLBrace(..) | Token::StrTemplate(..) => {
matches!(p.syntax, Syntax::Scss | Syntax::Sass)
}
Token::AtKeyword(..) | Token::AtLBraceVar(..) => {
p.syntax == Syntax::Less
}
_ => false,
})
);
if has_substitution {
Ok(raw)
} else {
let span = p.cursor.peek()?.span.clone();
Err(Error { kind: ErrorKind::TryParseError, span })
}
})
} else {
Err(Error {
kind: ErrorKind::TryParseError,
span: input.cursor.peek()?.span.clone(),
})
};
match raw {
Ok(raw) => Some(AtRulePrelude::Unknown(input.alloc(raw))),
Err(_) => Some(AtRulePrelude::Media(input.parse()?)),
}
}
};
let block = input.parse::<SimpleBlock>()?;
let end = block.span.end;
(prelude, Some(block), end)
} else if at_rule_name.eq_ignore_ascii_case("keyframes")
|| at_rule_name.eq_ignore_ascii_case("-webkit-keyframes")
|| at_rule_name.eq_ignore_ascii_case("-moz-keyframes")
|| at_rule_name.eq_ignore_ascii_case("-ms-keyframes")
|| at_rule_name.eq_ignore_ascii_case("-o-keyframes")
{
let prelude = match &input.cursor.peek()?.token {
Token::LBrace(..) => None,
_ => {
let typed = input.try_parse_full_prelude(KeyframesName::parse);
match typed {
Ok(name) => Some(AtRulePrelude::Keyframes(name)),
Err(_) => input
.parse_unknown_at_rule_prelude()?
.map(|prelude| AtRulePrelude::Unknown(input.alloc(prelude))),
}
}
};
let block = input
.with_state(ParserState { in_keyframes_at_rule: true, ..input.state.clone() })
.parse::<SimpleBlock>()?;
let end = block.span.end;
(prelude, Some(block), end)
} else if at_rule_name.eq_ignore_ascii_case("import") {
let (end, prelude) = match input.syntax {
Syntax::Css => {
let prelude = input.parse::<ImportPrelude>()?;
(prelude.span.end, AtRulePrelude::Import(input.alloc(prelude)))
}
Syntax::Scss | Syntax::Sass => {
let multi_path = input.try_parse(|parser| {
let prelude = parser.try_parse_full_prelude(SassImportPrelude::parse)?;
if prelude.paths.len() > 1 {
Ok(prelude)
} else {
Err(Error {
kind: ErrorKind::TryParseError,
span: prelude.span.clone(),
})
}
});
if let Ok(prelude) = multi_path {
(prelude.span.end, AtRulePrelude::SassImport(prelude))
} else if let Ok(prelude) = input.try_parse(ImportPrelude::parse) {
(prelude.span.end, AtRulePrelude::Import(input.alloc(prelude)))
} else {
let prelude = input.parse::<SassImportPrelude>()?;
(prelude.span.end, AtRulePrelude::SassImport(prelude))
}
}
Syntax::Less => {
if let Ok(prelude) = input.try_parse(ImportPrelude::parse) {
(prelude.span.end, AtRulePrelude::Import(input.alloc(prelude)))
} else {
let prelude = input.parse::<LessImportPrelude>()?;
(prelude.span.end, AtRulePrelude::LessImport(input.alloc(prelude)))
}
}
};
(Some(prelude), None, end)
} else if at_rule_name.eq_ignore_ascii_case("charset") {
if input.syntax == Syntax::Less
&& matches!(input.cursor.peek()?.token, Token::StrTemplate(..))
{
let prelude = input.parse::<InterpolableStr>()?;
let end = prelude.span().end;
(
Some(AtRulePrelude::Unknown(input.alloc(
UnknownAtRulePrelude::ComponentValue(ComponentValue::InterpolableStr(
prelude,
)),
))),
None,
end,
)
} else {
let prelude = input.parse::<Str>()?;
let end = prelude.span.end;
(Some(AtRulePrelude::Charset(prelude)), None, end)
}
} else if at_rule_name.eq_ignore_ascii_case("font-face") {
let block = input.parse::<SimpleBlock>()?;
let end = block.span.end;
(None, Some(block), end)
} else if at_rule_name.eq_ignore_ascii_case("supports") {
let prelude = Some(AtRulePrelude::Supports(input.parse()?));
let block = input.parse::<SimpleBlock>()?;
let end = block.span.end;
(prelude, Some(block), end)
} else if at_rule_name.eq_ignore_ascii_case("layer") {
let prelude = match input.try_parse(LayerNames::parse) {
Ok(names) => Some(AtRulePrelude::Layer(names)),
Err(_)
if input.syntax == Syntax::Less
&& matches!(input.cursor.peek()?.token, Token::AtKeyword(..)) =>
{
let raw = input.parse_raw_at_rule_prelude()?;
Some(AtRulePrelude::Unknown(input.alloc(raw)))
}
Err(_) => None,
};
let block =
if matches!(input.cursor.peek()?.token, Token::LBrace(..) | Token::Indent(..)) {
Some(input.parse::<SimpleBlock>()?)
} else {
None
};
if let Some(block) = &block
&& matches!(&prelude, Some(AtRulePrelude::Layer(names)) if names.names.len() > 1)
{
input.recoverable_errors.push(Error {
kind: ErrorKind::UnexpectedSimpleBlock,
span: block.span.clone(),
});
}
let end = block
.as_ref()
.map(|block| block.span.end)
.or_else(|| prelude.as_ref().map(|prelude| prelude.span().end))
.unwrap_or(at_keyword_span.end);
(prelude, block, end)
} else if at_rule_name.eq_ignore_ascii_case("container") {
let prelude =
match input.try_parse_full_prelude(|p| p.parse().map(AtRulePrelude::Container)) {
Ok(prelude) => prelude,
Err(error) => {
let is_query_list = input
.try_parse(|p| {
p.parse::<ContainerPrelude>()?;
if matches!(p.cursor.peek()?.token, Token::Comma(..)) {
Ok(())
} else {
let span = p.cursor.peek()?.span.clone();
Err(Error { kind: ErrorKind::TryParseError, span })
}
})
.is_ok();
let less_variable_name = input.syntax == Syntax::Less
&& matches!(input.cursor.peek()?.token, Token::AtKeyword(..));
if !is_query_list && !less_variable_name {
return Err(error);
}
let prelude = input.parse_raw_at_rule_prelude()?;
AtRulePrelude::Unknown(input.alloc(prelude))
}
};
let block = input.parse::<SimpleBlock>()?;
let end = block.span.end;
(Some(prelude), Some(block), end)
} else if at_rule_name.eq_ignore_ascii_case("page") {
let prelude = input.try_parse(PageSelectorList::parse).map(AtRulePrelude::Page).ok();
let block = input.try_parse(SimpleBlock::parse).ok();
let end = block
.as_ref()
.map(|block| block.span.end)
.or_else(|| prelude.as_ref().map(|prelude| prelude.span().end))
.unwrap_or(at_keyword_span.end);
(prelude, block, end)
} else if at_rule_name.eq_ignore_ascii_case("namespace")
&& input.syntax == Syntax::Less
&& matches!(input.cursor.peek()?.token, Token::AtKeyword(..))
{
let raw = input.parse_raw_at_rule_prelude()?;
let end = raw.span().end;
(Some(AtRulePrelude::Unknown(input.alloc(raw))), None, end)
} else if at_rule_name.eq_ignore_ascii_case("namespace") {
let namespace = input.parse::<NamespacePrelude>()?;
let end = namespace.span.end;
(Some(AtRulePrelude::Namespace(input.alloc(namespace))), None, end)
} else if at_rule_name.eq_ignore_ascii_case("color-profile") {
let prelude = Some(AtRulePrelude::ColorProfile(input.parse()?));
let block = input.parse::<SimpleBlock>()?;
let end = block.span.end;
(prelude, Some(block), end)
} else if at_rule_name.eq_ignore_ascii_case("font-feature-values") {
let prelude = Some(AtRulePrelude::FontFeatureValues(input.parse()?));
let block = input.parse::<SimpleBlock>()?;
let end = block.span.end;
(prelude, Some(block), end)
} else if at_rule_name.eq_ignore_ascii_case("font-palette-values") {
let prelude = Some(AtRulePrelude::FontPaletteValues(input.parse_dashed_ident()?));
let block = input.parse::<SimpleBlock>()?;
let end = block.span.end;
(prelude, Some(block), end)
} else if at_rule_name.eq_ignore_ascii_case("counter-style") {
let prelude = Some(AtRulePrelude::CounterStyle(input.parse_counter_style_prelude()?));
let block = input.parse::<SimpleBlock>()?;
let end = block.span.end;
(prelude, Some(block), end)
} else if at_rule_name.eq_ignore_ascii_case("custom-media") {
let custom_media = input.parse::<CustomMedia>()?;
let end = custom_media.span.end;
(Some(AtRulePrelude::CustomMedia(input.alloc(custom_media))), None, end)
} else if at_rule_name.eq_ignore_ascii_case("custom-selector") {
let custom_selector_prelude = input.parse::<CustomSelectorPrelude>()?;
let end = custom_selector_prelude.span.end;
(Some(AtRulePrelude::CustomSelector(input.alloc(custom_selector_prelude))), None, end)
} else if at_rule_name.eq_ignore_ascii_case("position-try") {
let prelude = Some(AtRulePrelude::PositionTry(input.parse_dashed_ident()?));
let block = input.parse::<SimpleBlock>()?;
let end = block.span.end;
(prelude, Some(block), end)
} else if at_rule_name.eq_ignore_ascii_case("nest") {
let prelude = Some(AtRulePrelude::Nest(input.parse()?));
let block = input.parse::<SimpleBlock>()?;
let end = block.span.end;
(prelude, Some(block), end)
} else if at_rule_name.eq_ignore_ascii_case("property") {
let prelude = Some(AtRulePrelude::Property(input.parse_dashed_ident()?));
let block = input.parse::<SimpleBlock>()?;
let end = block.span.end;
(prelude, Some(block), end)
} else if at_rule_name.eq_ignore_ascii_case("scope") {
let prelude = if let Token::LParen(..) | Token::Ident(..) = input.cursor.peek()?.token {
let scope = input.parse()?;
Some(AtRulePrelude::Scope(input.alloc(scope)))
} else {
None
};
let block = input.parse::<SimpleBlock>()?;
let end = block.span.end;
(prelude, Some(block), end)
} else if at_rule_name.eq_ignore_ascii_case("document")
|| at_rule_name.eq_ignore_ascii_case("-moz-document")
{
let prelude = match input.try_parse(|p| p.parse().map(AtRulePrelude::Document)) {
Ok(prelude) => prelude,
Err(error) => {
if matches!(
input.cursor.peek()?.token,
Token::LBrace(..) | Token::Indent(..) | Token::Semicolon(..)
) {
return Err(error);
}
let prelude = input.parse_raw_at_rule_prelude()?;
AtRulePrelude::Unknown(input.alloc(prelude))
}
};
let block =
if matches!(input.cursor.peek()?.token, Token::LBrace(..) | Token::Indent(..)) {
Some(input.parse::<SimpleBlock>()?)
} else {
None
};
let end = block.as_ref().map_or(prelude.span().end, |block| block.span.end);
(Some(prelude), block, end)
} else if at_rule_name.eq_ignore_ascii_case("stylistic")
|| at_rule_name.eq_ignore_ascii_case("historical-forms")
|| at_rule_name.eq_ignore_ascii_case("styleset")
|| at_rule_name.eq_ignore_ascii_case("character-variant")
|| at_rule_name.eq_ignore_ascii_case("swash")
|| at_rule_name.eq_ignore_ascii_case("ornaments")
|| at_rule_name.eq_ignore_ascii_case("annotation")
|| at_rule_name.eq_ignore_ascii_case("top-left-corner")
|| at_rule_name.eq_ignore_ascii_case("top-left")
|| at_rule_name.eq_ignore_ascii_case("top-center")
|| at_rule_name.eq_ignore_ascii_case("top-right")
|| at_rule_name.eq_ignore_ascii_case("top-right-corner")
|| at_rule_name.eq_ignore_ascii_case("bottom-left-corner")
|| at_rule_name.eq_ignore_ascii_case("bottom-left")
|| at_rule_name.eq_ignore_ascii_case("bottom-center")
|| at_rule_name.eq_ignore_ascii_case("bottom-right")
|| at_rule_name.eq_ignore_ascii_case("bottom-right-corner")
|| at_rule_name.eq_ignore_ascii_case("left-top")
|| at_rule_name.eq_ignore_ascii_case("left-middle")
|| at_rule_name.eq_ignore_ascii_case("left-bottom")
|| at_rule_name.eq_ignore_ascii_case("right-top")
|| at_rule_name.eq_ignore_ascii_case("right-middle")
|| at_rule_name.eq_ignore_ascii_case("right-bottom")
|| at_rule_name.eq_ignore_ascii_case("viewport")
|| at_rule_name.eq_ignore_ascii_case("try")
|| at_rule_name.eq_ignore_ascii_case("starting-style")
{
let block = input.parse::<SimpleBlock>()?;
let end = block.span.end;
(None, Some(block), end)
} else if at_rule_name == "plugin" && input.syntax == Syntax::Less {
let prelude = input.parse::<LessPlugin>()?;
let end = prelude.span.end;
(Some(AtRulePrelude::LessPlugin(input.alloc(prelude))), None, end)
} else if at_rule_name.eq_ignore_ascii_case("function")
&& matches!(&input.cursor.peek()?.token, Token::Ident(ident) if ident.raw.starts_with("--"))
{
let prelude = input.parse_raw_at_rule_prelude()?;
let block = input
.with_state(ParserState { in_css_function_body: true, ..input.state.clone() })
.parse::<SimpleBlock>()?;
let end = block.span.end;
(Some(AtRulePrelude::Unknown(input.alloc(prelude))), Some(block), end)
} else if matches!(input.syntax, Syntax::Scss | Syntax::Sass) {
use super::state::{
SASS_CTX_ALLOW_DIV, SASS_CTX_ALLOW_KEYFRAME_BLOCK, SASS_CTX_IN_FUNCTION,
};
match &*at_rule_name {
"each" => {
let prelude = input.parse()?;
let block = input.parse::<SimpleBlock>()?;
let end = block.span.end;
(Some(AtRulePrelude::SassEach(input.alloc(prelude))), Some(block), end)
}
"while" => {
input.eat_sass_line_continuation()?;
let prelude = input.parse()?;
let block = input.parse::<SimpleBlock>()?;
let end = block.span.end;
(Some(AtRulePrelude::SassExpr(input.alloc(prelude))), Some(block), end)
}
"for" => {
let prelude = input.parse()?;
let block = input.parse::<SimpleBlock>()?;
let end = block.span.end;
(Some(AtRulePrelude::SassFor(input.alloc(prelude))), Some(block), end)
}
"mixin" => {
let prelude = input.parse()?;
let block = input
.with_state(ParserState {
sass_ctx: input.state.sass_ctx | SASS_CTX_ALLOW_KEYFRAME_BLOCK,
..input.state.clone()
})
.parse::<SimpleBlock>()?;
let end = block.span.end;
(Some(AtRulePrelude::SassMixin(input.alloc(prelude))), Some(block), end)
}
"include" => {
let prelude = input.parse::<SassInclude>()?;
let block = if matches!(
input.cursor.peek()?.token,
Token::LBrace(..) | Token::Indent(..)
) {
Some(
input
.with_state(ParserState {
sass_ctx: input.state.sass_ctx | SASS_CTX_ALLOW_KEYFRAME_BLOCK,
..input.state.clone()
})
.parse::<SimpleBlock>()?,
)
} else {
None
};
let end =
block.as_ref().map(|block| block.span.end).unwrap_or(prelude.span.end);
(Some(AtRulePrelude::SassInclude(input.alloc(prelude))), block, end)
}
"content" => {
if matches!(input.cursor.peek()?.token, Token::LParen(..)) {
let prelude = input.parse::<SassContent>()?;
let end = prelude.span.end;
(Some(AtRulePrelude::SassContent(prelude)), None, end)
} else {
(None, None, input.cursor.tokenizer.current_offset())
}
}
"use" => {
let prelude = input.parse::<SassUse>()?;
let end = prelude.span.end;
(Some(AtRulePrelude::SassUse(input.alloc(prelude))), None, end)
}
"function" => {
let prelude = input.parse::<SassFunction>()?;
let block = input
.with_state(ParserState {
sass_ctx: input.state.sass_ctx | SASS_CTX_IN_FUNCTION,
..input.state.clone()
})
.parse::<SimpleBlock>()?;
let end = block.span.end;
(Some(AtRulePrelude::SassFunction(input.alloc(prelude))), Some(block), end)
}
"return" => {
input.eat_sass_line_continuation()?;
let expr = input
.with_state(ParserState {
sass_ctx: input.state.sass_ctx | SASS_CTX_ALLOW_DIV,
..input.state.clone()
})
.parse_maybe_sass_list( true)?;
let end = expr.span().end;
if input.state.sass_ctx & SASS_CTX_IN_FUNCTION == 0 {
input.recoverable_errors.push(Error {
kind: ErrorKind::ReturnOutsideFunction,
span: Span { start: at_keyword_span.start, end },
});
}
(Some(AtRulePrelude::SassExpr(input.alloc(expr))), None, end)
}
"extend" => {
let prelude = input.parse::<SassExtend>()?;
let end = prelude.span.end;
(Some(AtRulePrelude::SassExtend(input.alloc(prelude))), None, end)
}
"warn" | "error" | "debug" => {
input.eat_sass_line_continuation()?;
let expr = input.parse_maybe_sass_list( true)?;
let end = expr.span().end;
(Some(AtRulePrelude::SassExpr(input.alloc(expr))), None, end)
}
"forward" => {
let prelude = input.parse::<SassForward>()?;
let end = prelude.span.end;
(Some(AtRulePrelude::SassForward(input.alloc(prelude))), None, end)
}
"at-root" => {
let prelude = if !matches!(
input.cursor.peek()?.token,
Token::LBrace(..)
| Token::Indent(..)
| Token::Linebreak(..)
| Token::Dedent(..)
| Token::Eof(..)
) {
Some(AtRulePrelude::SassAtRoot(input.parse()?))
} else {
None
};
let block = input
.with_state(ParserState {
in_keyframes_at_rule: false,
..input.state.clone()
})
.parse::<SimpleBlock>()?;
let end = block.span.end;
(prelude, Some(block), end)
}
_ => {
let (prelude, block, end) = input.parse_unknown_at_rule()?;
(
prelude.map(|prelude| AtRulePrelude::Unknown(input.alloc(prelude))),
block,
end.unwrap_or(at_keyword_span.end),
)
}
}
} else {
let (prelude, block, end) = input.parse_unknown_at_rule()?;
(
prelude.map(|prelude| AtRulePrelude::Unknown(input.alloc(prelude))),
block,
end.unwrap_or(at_keyword_span.end),
)
};
let span = Span { start: at_keyword_span.start, end };
Ok(AtRule {
name: input.ident(
at_keyword.ident,
Span { start: at_keyword_span.start + 1, end: at_keyword_span.end },
),
prelude,
block,
span,
})
}
}
impl<'a> Parser<'a> {
fn try_parse_full_prelude<T>(&mut self, f: impl FnOnce(&mut Self) -> PResult<T>) -> PResult<T> {
self.try_parse(|p| {
let value = f(p)?;
match &p.cursor.peek()?.token {
Token::LBrace(..)
| Token::Indent(..)
| Token::Semicolon(..)
| Token::Dedent(..)
| Token::Linebreak(..)
| Token::Eof(..) => Ok(value),
_ => {
let span = p.cursor.peek()?.span.clone();
Err(Error { kind: ErrorKind::TryParseError, span })
}
}
})
}
fn parse_raw_at_rule_prelude(&mut self) -> PResult<UnknownAtRulePrelude<'a>> {
let start = self.cursor.tokenizer.current_offset();
let mut tokens = self.vec();
let mut pairs: Vec<crate::util::PairedToken> = Vec::new();
loop {
match &self.cursor.peek()?.token {
Token::Semicolon(..)
| Token::Dedent(..)
| Token::Linebreak(..)
| Token::Indent(..)
| Token::Eof(..) => break,
Token::LBrace(..) if pairs.is_empty() => break,
Token::StrTemplate(..) => {
self.consume_str_template_tokens_into(&mut tokens)?;
continue;
}
token => {
if !crate::util::track_paired_token(token, &mut pairs) {
break;
}
}
}
tokens.push(self.cursor.bump()?);
}
let span = Span {
start: tokens.first().map_or(start, |token| token.span.start),
end: tokens.last().map_or(start, |token| token.span.end),
};
Ok(UnknownAtRulePrelude::TokenSeq(TokenSeq { tokens, span }))
}
pub(super) fn parse_unknown_at_rule(
&mut self,
) -> PResult<(Option<UnknownAtRulePrelude<'a>>, Option<SimpleBlock<'a>>, Option<usize>)> {
let prelude = self.parse_unknown_at_rule_prelude()?;
let block = match &self.cursor.peek()?.token {
Token::LBrace(..) | Token::Indent(..) => {
let sass_ctx = if matches!(self.syntax, Syntax::Scss | Syntax::Sass) {
self.state.sass_ctx | super::state::SASS_CTX_ALLOW_KEYFRAME_BLOCK
} else {
self.state.sass_ctx
};
Some(
self.with_state(ParserState { sass_ctx, ..self.state.clone() })
.parse::<SimpleBlock>()?,
)
}
_ => None,
};
let end = block
.as_ref()
.map(|block| block.span.end)
.or_else(|| prelude.as_ref().map(|prelude| prelude.span().end));
Ok((prelude, block, end))
}
fn parse_unknown_at_rule_prelude(&mut self) -> PResult<Option<UnknownAtRulePrelude<'a>>> {
if let Ok(prelude) = self.try_parse(|parser| {
let mut tokens = parser.vec();
loop {
match &parser.cursor.peek()?.token {
Token::LBrace(..)
| Token::RBrace(..)
| Token::Semicolon(..)
| Token::Indent(..)
| Token::Dedent(..)
| Token::Linebreak(..)
| Token::Eof(..) => break,
Token::StrTemplate(..) | Token::HashLBrace(..) => {
return Err(Error {
kind: ErrorKind::TryParseError,
span: parser.cursor.bump()?.span,
});
}
_ => tokens.push(parser.cursor.bump()?),
}
}
if let Some((first, last)) = tokens.first().zip(tokens.last()) {
let span = Span { start: first.span.start, end: last.span.end };
Ok(Some(UnknownAtRulePrelude::TokenSeq(TokenSeq { tokens, span })))
} else {
Ok(None)
}
}) {
return Ok(prelude);
}
Ok(Some(UnknownAtRulePrelude::ComponentValue(match self.syntax {
Syntax::Css => self.parse()?,
Syntax::Scss | Syntax::Sass => {
self.parse_maybe_sass_list( true)?
}
Syntax::Less => self.parse_maybe_less_list( true)?,
})))
}
}