mod preludes;
mod raw;
pub(super) use super::CssParser;
use crate::ast::*;
use crate::lexer::TokenKind;
use tsv_lang::{Error, Span};
use self::preludes::{
ConditionReader, parse_condition_query, parse_container_prelude, parse_custom_selector_prelude,
parse_import_prelude, parse_scope_prelude,
};
use self::raw::parse_raw_prelude_content;
pub(crate) fn is_keyframes_atrule(name: &str) -> bool {
name.eq_ignore_ascii_case("keyframes")
|| name.eq_ignore_ascii_case("-webkit-keyframes")
|| name.eq_ignore_ascii_case("-moz-keyframes")
|| name.eq_ignore_ascii_case("-o-keyframes")
|| name.eq_ignore_ascii_case("-ms-keyframes")
}
pub(super) fn is_boolean_operator_keyword(ident: &str) -> bool {
ident.eq_ignore_ascii_case("and")
|| ident.eq_ignore_ascii_case("or")
|| ident.eq_ignore_ascii_case("not")
}
pub(super) fn is_boolean_operator(parser: &CssParser<'_, '_>) -> bool {
if let TokenKind::Identifier = parser.current_kind() {
is_boolean_operator_keyword(parser.current_identifier())
} else {
false
}
}
pub(crate) fn parse_atrule<'arena>(
parser: &mut CssParser<'_, 'arena>,
nested_in_rule: bool,
) -> Result<CssAtrule<'arena>, Error> {
parser.nested(2, |p| parse_atrule_body(p, nested_in_rule))
}
fn parse_atrule_body<'arena>(
parser: &mut CssParser<'_, 'arena>,
nested_in_rule: bool,
) -> Result<CssAtrule<'arena>, Error> {
let start = parser.span_pos(parser.current_start());
parser.expect(TokenKind::AtSign)?;
if !parser.check(TokenKind::Identifier) {
return Err(parser.error_expected_after("at-rule name", "@"));
}
let name = parser.current_identifier_in_arena();
let name_span = Span {
start: parser.span_pos(parser.current_start()),
end: parser.span_pos(parser.current_end()),
};
parser.advance()?;
parser.skip_whitespace()?;
let name_lc_owned;
let name_lc: &str = if name.bytes().any(|b| b.is_ascii_uppercase()) {
name_lc_owned = name.to_ascii_lowercase();
&name_lc_owned
} else {
name
};
let prelude_start_raw = parser.current_start();
let prelude = if name_lc == "import" {
parse_import_prelude(parser)?
} else if name_lc == "scope" {
parse_scope_prelude(parser)?
} else if name_lc == "custom-selector" {
parse_custom_selector_prelude(parser)?
} else if name_lc == "supports" {
let (condition, span) = parse_condition_query(parser, ConditionReader::Value)?;
PreludeValue::Supports { condition, span }
} else if name_lc == "container" {
let (name, condition, span) = parse_container_prelude(parser)?;
PreludeValue::Container {
name,
condition,
span,
}
} else if matches!(name_lc, "media" | "custom-media") {
let (content, span) = parse_raw_prelude_content(parser, true, false)?;
PreludeValue::Media { content, span }
} else {
let is_namespace = name_lc == "namespace";
let (content, span) = parse_raw_prelude_content(parser, is_namespace, is_namespace)?;
PreludeValue::Raw { content, span }
};
let partless_with_run = match &prelude {
PreludeValue::Supports { condition, .. } | PreludeValue::Container { condition, .. } => {
condition.parts.is_empty()
&& crate::whitespace::holds_boundary_member(
parser.source(),
prelude_start_raw,
parser.current_start(),
)
}
_ => false,
};
let prelude = if matches!(name_lc, "supports" | "container")
&& (!parser.at_prelude_end() || partless_with_run)
{
reconsume_prelude_as_raw(parser, prelude_start_raw)?
} else {
prelude
};
let (block, end) = if parser.check(TokenKind::LeftBrace) {
let block = parse_atrule_block(parser, name_lc, nested_in_rule)?;
let end = block.span.end;
(Some(block), end)
} else if parser.check(TokenKind::Semicolon) {
let end = parser.span_pos(parser.current_end());
parser.advance()?;
return Ok(CssAtrule {
name,
name_span,
prelude,
block: None,
span: Span { start, end },
});
} else {
return Err(parser.error_expected_after("'{' or ';'", "at-rule prelude"));
};
Ok(CssAtrule {
name,
name_span,
prelude,
block,
span: Span { start, end },
})
}
pub(super) fn reconsume_prelude_as_raw<'arena>(
parser: &mut CssParser<'_, 'arena>,
prelude_start_raw: usize,
) -> Result<PreludeValue<'arena>, Error> {
let comments_len = parser.comments.len();
parser.rewind_to(prelude_start_raw, comments_len)?;
let mut ends = raw::PreludeEnds::default();
while !parser.at_prelude_end() {
ends.observe(parser, parser.current_start() - prelude_start_raw, 0);
parser.advance()?;
}
let raw = &parser.source()[prelude_start_raw..parser.current_start()];
let lead = crate::escapes::trim_start_css(raw);
let trimmed = crate::escapes::trim_end_preserving_escape(lead);
let content_start_raw = prelude_start_raw + (raw.len() - lead.len());
let content_end_raw = content_start_raw + trimmed.len();
let respelled = ends.respell(
parser.source(),
prelude_start_raw,
parser.current_start(),
raw,
);
let content = respelled
.as_deref()
.map_or(trimmed, |respelled| crate::escapes::trim_css(respelled));
Ok(PreludeValue::Raw {
content: parser.alloc_str_in(content),
span: Span {
start: parser.span_pos(content_start_raw),
end: parser.span_pos(content_end_raw),
},
})
}
fn parse_atrule_block<'arena>(
parser: &mut CssParser<'_, 'arena>,
atrule_name: &str,
nested_in_rule: bool,
) -> Result<CssAtruleBlock<'arena>, Error> {
let start = parser.span_pos(parser.current_start());
parser.expect(TokenKind::LeftBrace)?;
parser.skip_boundary_whitespace()?;
let mut children = parser.bvec();
let expect_rules = (matches!(
atrule_name,
"media" | "supports" | "layer" | "container" | "starting-style" | "font-feature-values"
) || is_keyframes_atrule(atrule_name))
&& !nested_in_rule;
while !parser.check(TokenKind::RightBrace) && !parser.check(TokenKind::Eof) {
if matches!(parser.current_kind(), TokenKind::Comment) {
let comment = parser.parse_block_comment()?;
children.push(CssBlockChild::Comment(comment));
continue;
}
if parser.check(TokenKind::AtSign) {
let atrule = parse_atrule(parser, nested_in_rule)?;
children.push(CssBlockChild::Atrule(atrule));
parser.skip_boundary_whitespace()?;
continue;
}
if expect_rules {
children.push(parse_block_child(parser, false, atrule_name)?);
parser.skip_boundary_whitespace()?;
continue;
}
children.push(parse_block_child(parser, true, atrule_name)?);
parser.skip_boundary_whitespace()?;
}
if !parser.check(TokenKind::RightBrace) {
return Err(parser.error_expected("'}'"));
}
let end = parser.span_pos(parser.current_end());
parser.advance()?;
Ok(CssAtruleBlock {
children: children.into_bump_slice(),
span: Span { start, end },
})
}
fn parse_block_child<'arena>(
parser: &mut CssParser<'_, 'arena>,
allow_relative_selectors: bool,
atrule_name: &str,
) -> Result<CssBlockChild<'arena>, Error> {
if super::declarations::is_nested_rule_start(parser)? {
Ok(CssBlockChild::Rule(super::declarations::parse_rule(
parser,
allow_relative_selectors,
)?))
} else if parser.check(TokenKind::Identifier) {
Ok(CssBlockChild::Declaration(
super::declarations::parse_declaration(parser)?,
))
} else {
Err(parser.error_unexpected(&format!("token in @{atrule_name} block")))
}
}