use cstree::{
Syntax,
build::GreenNodeBuilder,
green::GreenNode,
interning::TokenInterner,
syntax::SyntaxNode,
text::{TextRange, TextSize},
};
use omena_interner::{
NameKind, intern_class_name, intern_css_ident, intern_custom_property_name, intern_file_path,
intern_keyframes_name, intern_mixin_name, intern_property_name, intern_selector_key,
};
pub use omena_syntax::StyleDialect;
use omena_syntax::SyntaxKind;
use serde::{Deserialize, Serialize};
use std::{
collections::{BTreeMap, BTreeSet},
sync::Arc,
};
mod public_product;
pub use public_product::{
ParserCanonicalCandidateBundleV0, ParserCanonicalProducerSignalV0, ParserEvaluatorCandidatesV0,
ParserIndexSummaryV0, dialect_for_path, summarize_css_modules_intermediate,
summarize_parser_canonical_candidate, summarize_parser_canonical_producer_signal,
summarize_parser_evaluator_candidates,
};
const VALUES_L4_MATH_FUNCTION_NAMES: &[&str] = &[
"min", "max", "clamp", "round", "mod", "rem", "sin", "cos", "tan", "asin", "acos", "atan",
"atan2", "pow", "sqrt", "hypot", "log", "exp", "abs", "sign",
];
const CSS_COLOR_FUNCTION_NAMES: &[&str] = &[
"rgb",
"rgba",
"hsl",
"hsla",
"hwb",
"lab",
"lch",
"oklab",
"oklch",
"color",
"color-mix",
"device-cmyk",
"light-dark",
"contrast-color",
];
const CSS_GRADIENT_FUNCTION_NAMES: &[&str] = &[
"linear-gradient",
"radial-gradient",
"conic-gradient",
"repeating-linear-gradient",
"repeating-radial-gradient",
"repeating-conic-gradient",
];
const CSS_TRANSFORM_FUNCTION_NAMES: &[&str] = &[
"matrix",
"matrix3d",
"translate",
"translate3d",
"translateX",
"translateY",
"translateZ",
"scale",
"scale3d",
"scaleX",
"scaleY",
"scaleZ",
"rotate",
"rotate3d",
"rotateX",
"rotateY",
"rotateZ",
"skew",
"skewX",
"skewY",
"perspective",
];
const CSS_FILTER_FUNCTION_NAMES: &[&str] = &[
"blur",
"brightness",
"contrast",
"drop-shadow",
"grayscale",
"hue-rotate",
"invert",
"opacity",
"saturate",
"sepia",
];
const CSS_IMAGE_FUNCTION_NAMES: &[&str] = &["image", "image-set", "cross-fade", "element", "paint"];
const CSS_SHAPE_FUNCTION_NAMES: &[&str] = &[
"path", "shape", "ray", "inset", "circle", "ellipse", "polygon",
];
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct ParserByteSpanV0 {
pub start: usize,
pub end: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct ParserPositionV0 {
pub line: usize,
pub character: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct ParserRangeV0 {
pub start: ParserPositionV0,
pub end: ParserPositionV0,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StyleLanguage {
Css,
Scss,
Less,
}
impl StyleLanguage {
pub fn from_module_path(path: &str) -> Option<Self> {
if path.ends_with(".module.css") || path.ends_with(".css") {
Some(Self::Css)
} else if path.ends_with(".module.scss") || path.ends_with(".scss") {
Some(Self::Scss)
} else if path.ends_with(".module.less") || path.ends_with(".less") {
Some(Self::Less)
} else {
None
}
}
}
#[derive(Debug, Clone)]
pub struct ParseResult {
green: GreenNode,
interner: Option<Arc<TokenInterner>>,
errors: Vec<ParseError>,
token_count: usize,
dialect: StyleDialect,
}
impl PartialEq for ParseResult {
fn eq(&self, other: &Self) -> bool {
self.green == other.green
&& self.errors == other.errors
&& self.token_count == other.token_count
&& self.dialect == other.dialect
}
}
impl Eq for ParseResult {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LexResult {
tokens: Vec<LexedToken>,
errors: Vec<ParseError>,
dialect: StyleDialect,
}
impl LexResult {
pub fn tokens(&self) -> &[LexedToken] {
&self.tokens
}
pub fn errors(&self) -> &[ParseError] {
&self.errors
}
pub fn dialect(&self) -> StyleDialect {
self.dialect
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LexedToken {
pub kind: SyntaxKind,
pub range: TextRange,
pub text: String,
}
impl ParseResult {
pub fn green(&self) -> &GreenNode {
&self.green
}
pub fn syntax(&self) -> SyntaxNode<SyntaxKind> {
if let Some(interner) = &self.interner {
return SyntaxNode::new_root_with_resolver(self.green.clone(), Arc::clone(interner))
.syntax()
.clone();
}
SyntaxNode::new_root(self.green.clone())
}
pub fn source_text(&self) -> Option<String> {
let syntax = self.syntax();
syntax
.try_resolved()
.map(|resolved| resolved.text().to_string())
}
pub fn errors(&self) -> &[ParseError] {
&self.errors
}
pub fn token_count(&self) -> usize {
self.token_count
}
pub fn dialect(&self) -> StyleDialect {
self.dialect
}
pub fn cst(&self) -> ParsedCst {
ParsedCst::new(self.syntax())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseError {
pub code: ParseErrorCode,
pub range: TextRange,
pub message: &'static str,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ParseErrorCode {
UnterminatedBlockComment,
UnterminatedString,
UnexpectedCharacter,
ExpectedSelectorName,
UnterminatedAttributeSelector,
ExpectedValue,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ParseEntryPoint {
Stylesheet,
RuleList,
Rule,
DeclarationList,
Declaration,
Value,
ComponentValue,
ComponentValueList,
CommaSeparatedComponentValueList,
SimpleBlock,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParserBoundarySummary {
pub product: &'static str,
pub tree_model: &'static str,
pub parser_track: &'static str,
pub dialect_count: usize,
pub shared_name_kind_count: usize,
pub ready_surfaces: Vec<&'static str>,
pub not_ready_surfaces: Vec<&'static str>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParserSemanticNameConsumptionSummaryV0 {
pub product: &'static str,
pub dialect: StyleDialect,
pub semantic_name_count: usize,
pub interned_name_count: usize,
pub invalid_name_count: usize,
pub class_name_count: usize,
pub css_ident_count: usize,
pub property_name_count: usize,
pub selector_key_count: usize,
pub custom_property_name_count: usize,
pub keyframes_name_count: usize,
pub mixin_name_count: usize,
pub file_path_count: usize,
pub ready_surfaces: Vec<&'static str>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParserCstEquivalenceSummaryV0 {
pub product: &'static str,
pub dialect: StyleDialect,
pub root_kind: SyntaxKind,
pub parser_node_count: usize,
pub parser_token_count: usize,
pub typed_wrapper_count: usize,
pub source_text_round_trip_ready: bool,
pub syntax_kind_round_trip_ready: bool,
pub zero_unknown_kind_ready: bool,
pub typed_cst_wrapper_ready: bool,
pub ready_surfaces: Vec<&'static str>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParserPrattValueCoverageSummaryV0 {
pub product: &'static str,
pub infix_operator_kinds: Vec<SyntaxKind>,
pub prefix_operator_kinds: Vec<SyntaxKind>,
pub value_expression_node_kinds: Vec<SyntaxKind>,
pub specialized_function_family_count: usize,
pub css_values_l4_math_function_count: usize,
pub css_color_function_count: usize,
pub ready_surfaces: Vec<&'static str>,
pub next_surfaces: Vec<&'static str>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParserRecursiveDescentCoverageSummaryV0 {
pub product: &'static str,
pub dialect_count: usize,
pub entry_point_count: usize,
pub selector_surface_count: usize,
pub at_rule_surface_count: usize,
pub dialect_extension_surface_count: usize,
pub recovery_surface_count: usize,
pub ready_surfaces: Vec<&'static str>,
pub next_surfaces: Vec<&'static str>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct ParserSemanticNameCandidateV0 {
kind: NameKind,
text: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsedStyleFacts {
pub product: &'static str,
pub dialect: StyleDialect,
pub selector_count: usize,
pub selectors: Vec<ParsedSelectorFact>,
pub variable_count: usize,
pub variables: Vec<ParsedVariableFact>,
pub sass_symbol_count: usize,
pub sass_symbols: Vec<ParsedSassSymbolFact>,
pub sass_include_count: usize,
pub sass_includes: Vec<ParsedSassIncludeFact>,
pub sass_module_edge_count: usize,
pub sass_module_edges: Vec<ParsedSassModuleEdgeFact>,
pub animation_count: usize,
pub animations: Vec<ParsedAnimationFact>,
pub css_module_value_count: usize,
pub css_module_values: Vec<ParsedCssModuleValueFact>,
pub css_module_value_import_edge_count: usize,
pub css_module_value_import_edges: Vec<ParsedCssModuleValueImportEdgeFact>,
pub css_module_value_definition_edge_count: usize,
pub css_module_value_definition_edges: Vec<ParsedCssModuleValueDefinitionEdgeFact>,
pub css_module_composes_count: usize,
pub css_module_composes: Vec<ParsedCssModuleComposesFact>,
pub css_module_composes_edge_count: usize,
pub css_module_composes_edges: Vec<ParsedCssModuleComposesEdgeFact>,
pub icss_count: usize,
pub icss: Vec<ParsedIcssFact>,
pub icss_import_edge_count: usize,
pub icss_import_edges: Vec<ParsedIcssImportEdgeFact>,
pub icss_export_edge_count: usize,
pub icss_export_edges: Vec<ParsedIcssExportEdgeFact>,
pub at_rule_count: usize,
pub at_rules: Vec<ParsedAtRuleFact>,
pub error_count: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsedSelectorFact {
pub kind: ParsedSelectorFactKind,
pub name: String,
pub range: TextRange,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum ParsedSelectorFactKind {
Class,
Id,
Placeholder,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsedVariableFact {
pub kind: ParsedVariableFactKind,
pub name: String,
pub range: TextRange,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ParsedVariableFactKind {
ScssDeclaration,
ScssReference,
LessDeclaration,
LessReference,
CustomPropertyDeclaration,
CustomPropertyReference,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsedSassSymbolFact {
pub kind: ParsedSassSymbolFactKind,
pub symbol_kind: &'static str,
pub name: String,
pub role: &'static str,
pub namespace: Option<String>,
pub range: TextRange,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum ParsedSassSymbolFactKind {
VariableDeclaration,
VariableReference,
MixinDeclaration,
MixinInclude,
FunctionDeclaration,
FunctionCall,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsedSassIncludeFact {
pub name: String,
pub namespace: Option<String>,
pub params: String,
pub range: TextRange,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsedSassModuleEdgeFact {
pub kind: ParsedSassModuleEdgeFactKind,
pub source: String,
pub namespace_kind: Option<&'static str>,
pub namespace: Option<String>,
pub forward_prefix: Option<String>,
pub visibility_filter_kind: Option<&'static str>,
pub visibility_filter_names: Vec<String>,
pub range: TextRange,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum ParsedSassModuleEdgeFactKind {
Use,
Forward,
Import,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsedAnimationFact {
pub kind: ParsedAnimationFactKind,
pub name: String,
pub range: TextRange,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum ParsedAnimationFactKind {
KeyframesDeclaration,
AnimationNameReference,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsedCssModuleValueFact {
pub kind: ParsedCssModuleValueFactKind,
pub name: String,
pub range: TextRange,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum ParsedCssModuleValueFactKind {
Definition,
Reference,
ImportSource,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsedCssModuleValueImportEdgeFact {
pub remote_name: String,
pub local_name: String,
pub import_source: String,
pub local_range: TextRange,
pub remote_range: TextRange,
pub range: TextRange,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsedCssModuleValueDefinitionEdgeFact {
pub definition_name: String,
pub reference_names: Vec<String>,
pub range: TextRange,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsedCssModuleComposesFact {
pub kind: ParsedCssModuleComposesFactKind,
pub name: String,
pub range: TextRange,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum ParsedCssModuleComposesFactKind {
Target,
ImportSource,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsedCssModuleComposesEdgeFact {
pub kind: ParsedCssModuleComposesEdgeKind,
pub owner_selector_names: Vec<String>,
pub target_names: Vec<String>,
pub import_source: Option<String>,
pub range: TextRange,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum ParsedCssModuleComposesEdgeKind {
Local,
Global,
External,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsedIcssFact {
pub kind: ParsedIcssFactKind,
pub name: String,
pub range: TextRange,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum ParsedIcssFactKind {
ExportName,
ImportLocalName,
ImportRemoteName,
ImportSource,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsedIcssImportEdgeFact {
pub local_name: String,
pub remote_name: String,
pub import_source: String,
pub range: TextRange,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsedIcssExportEdgeFact {
pub export_name: String,
pub reference_names: Vec<String>,
pub range: TextRange,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsedAtRuleFact {
pub name: String,
pub node_kind: Option<SyntaxKind>,
pub range: TextRange,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OmenaParserStyleFactsSummaryV0 {
pub schema_version: &'static str,
pub product: &'static str,
pub dialect: &'static str,
pub class_selector_names: Vec<String>,
pub id_selector_names: Vec<String>,
pub placeholder_selector_names: Vec<String>,
pub keyframe_names: Vec<String>,
pub animation_reference_names: Vec<String>,
pub css_module_value_definition_names: Vec<String>,
pub css_module_value_reference_names: Vec<String>,
pub css_module_value_import_sources: Vec<String>,
pub css_module_value_import_edges: Vec<OmenaParserCssModuleValueImportEdgeFactV0>,
pub css_module_value_definition_edges: Vec<OmenaParserCssModuleValueDefinitionEdgeFactV0>,
pub css_module_composes_target_names: Vec<String>,
pub css_module_composes_import_sources: Vec<String>,
pub css_module_composes_edges: Vec<OmenaParserCssModuleComposesEdgeFactV0>,
pub icss_export_names: Vec<String>,
pub icss_import_local_names: Vec<String>,
pub icss_import_remote_names: Vec<String>,
pub icss_import_sources: Vec<String>,
pub icss_import_edges: Vec<OmenaParserIcssImportEdgeFactV0>,
pub icss_export_edges: Vec<OmenaParserIcssExportEdgeFactV0>,
pub variable_names: Vec<String>,
pub sass_symbol_declaration_names: Vec<String>,
pub sass_symbol_reference_names: Vec<String>,
pub sass_symbol_facts: Vec<OmenaParserSassSymbolFactV0>,
pub sass_symbol_resolution: OmenaParserSassSymbolResolutionV0,
pub sass_module_use_sources: Vec<String>,
pub sass_module_forward_sources: Vec<String>,
pub sass_module_import_sources: Vec<String>,
pub sass_module_edges: Vec<OmenaParserSassModuleEdgeFactV0>,
pub custom_property_names: Vec<String>,
pub custom_property_decl_names: Vec<String>,
pub custom_property_ref_names: Vec<String>,
pub at_rule_names: Vec<String>,
pub parser_error_count: usize,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OmenaParserCssModuleValueImportEdgeFactV0 {
pub remote_name: String,
pub local_name: String,
pub import_source: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OmenaParserCssModuleValueDefinitionEdgeFactV0 {
pub definition_name: String,
pub reference_names: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OmenaParserCssModuleComposesEdgeFactV0 {
pub kind: &'static str,
pub owner_selector_names: Vec<String>,
pub target_names: Vec<String>,
pub import_source: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OmenaParserIcssImportEdgeFactV0 {
pub local_name: String,
pub remote_name: String,
pub import_source: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OmenaParserIcssExportEdgeFactV0 {
pub export_name: String,
pub reference_names: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OmenaParserSassSymbolFactV0 {
pub kind: &'static str,
pub symbol_kind: &'static str,
pub name: String,
pub role: &'static str,
pub namespace: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OmenaParserSassModuleEdgeFactV0 {
pub kind: &'static str,
pub source: String,
pub namespace_kind: Option<&'static str>,
pub namespace: Option<String>,
pub visibility_filter_kind: Option<&'static str>,
pub visibility_filter_names: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OmenaParserSassSymbolResolutionV0 {
pub schema_version: &'static str,
pub product: &'static str,
pub resolution_scope: &'static str,
pub declaration_count: usize,
pub reference_count: usize,
pub resolved_reference_count: usize,
pub unresolved_reference_count: usize,
pub edges: Vec<OmenaParserSassSymbolResolutionEdgeV0>,
pub capabilities: OmenaParserSassSymbolResolutionCapabilitiesV0,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OmenaParserSassSymbolResolutionEdgeV0 {
pub symbol_kind: &'static str,
pub name: String,
pub namespace: Option<String>,
pub reference_kind: &'static str,
pub reference_role: &'static str,
pub reference_source_order: usize,
pub declaration_kind: Option<&'static str>,
pub declaration_source_order: Option<usize>,
pub status: &'static str,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OmenaParserSassSymbolResolutionCapabilitiesV0 {
pub same_file_lexical_resolution_ready: bool,
pub declaration_before_reference_ready: bool,
pub unresolved_reference_reporting_ready: bool,
pub cross_file_module_resolution_ready: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OmenaParserLexSummaryV0 {
pub schema_version: &'static str,
pub product: &'static str,
pub dialect: &'static str,
pub tokens: Vec<OmenaParserLexTokenV0>,
pub parser_error_count: usize,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OmenaParserLexTokenV0 {
pub kind: String,
pub text: String,
pub start: usize,
pub end: usize,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OmenaParserParityLiteSummaryV0 {
pub schema_version: &'static str,
pub language: &'static str,
pub selector_names: Vec<String>,
pub keyframes_names: Vec<String>,
pub value_decl_names: Vec<String>,
pub diagnostic_count: usize,
pub rule_count: usize,
pub declaration_count: usize,
pub grouped_selector_count: usize,
pub max_nesting_depth: usize,
pub at_rule_kind_counts: OmenaParserAtRuleKindCountsV0,
pub declaration_kind_counts: OmenaParserDeclarationKindCountsV0,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OmenaParserAtRuleKindCountsV0 {
pub media: usize,
pub supports: usize,
pub layer: usize,
pub keyframes: usize,
pub value: usize,
pub at_root: usize,
pub generic: usize,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OmenaParserDeclarationKindCountsV0 {
pub composes: usize,
pub animation: usize,
pub animation_name: usize,
pub generic: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsedCst {
root: SyntaxNode<SyntaxKind>,
}
impl ParsedCst {
pub fn new(root: SyntaxNode<SyntaxKind>) -> Self {
Self { root }
}
pub fn root(&self) -> &SyntaxNode<SyntaxKind> {
&self.root
}
pub fn stylesheet(&self) -> Option<StylesheetCstNode> {
self.first_node(StylesheetCstNode::cast)
}
pub fn rules(&self) -> Vec<RuleCstNode> {
self.nodes(RuleCstNode::cast)
}
pub fn selectors(&self) -> Vec<SelectorCstNode> {
self.nodes(SelectorCstNode::cast)
}
pub fn declarations(&self) -> Vec<DeclarationCstNode> {
self.nodes(DeclarationCstNode::cast)
}
pub fn declaration_lists(&self) -> Vec<DeclarationListCstNode> {
self.nodes(DeclarationListCstNode::cast)
}
pub fn values(&self) -> Vec<ValueCstNode> {
self.nodes(ValueCstNode::cast)
}
pub fn component_values(&self) -> Vec<ComponentValueCstNode> {
self.nodes(ComponentValueCstNode::cast)
}
pub fn simple_blocks(&self) -> Vec<SimpleBlockCstNode> {
self.nodes(SimpleBlockCstNode::cast)
}
pub fn component_value_lists(&self) -> Vec<ComponentValueListCstNode> {
self.nodes(ComponentValueListCstNode::cast)
}
pub fn comma_separated_component_value_lists(
&self,
) -> Vec<CommaSeparatedComponentValueListCstNode> {
self.nodes(CommaSeparatedComponentValueListCstNode::cast)
}
pub fn custom_property_values(&self) -> Vec<CustomPropertyValueCstNode> {
self.nodes(CustomPropertyValueCstNode::cast)
}
pub fn at_rules(&self) -> Vec<AtRuleCstNode> {
self.nodes(AtRuleCstNode::cast)
}
pub fn bogus_nodes(&self) -> Vec<BogusCstNode> {
self.nodes(BogusCstNode::cast)
}
pub fn has_bogus_nodes(&self) -> bool {
self.first_node(BogusCstNode::cast).is_some()
}
fn first_node<T>(&self, cast: impl Fn(SyntaxNode<SyntaxKind>) -> Option<T>) -> Option<T> {
let mut nodes = Vec::new();
collect_typed_nodes(&self.root, &cast, &mut nodes);
nodes.into_iter().next()
}
fn nodes<T>(&self, cast: impl Fn(SyntaxNode<SyntaxKind>) -> Option<T>) -> Vec<T> {
let mut nodes = Vec::new();
collect_typed_nodes(&self.root, &cast, &mut nodes);
nodes
}
}
pub trait TypedCstNode: Sized {
fn cast(syntax: SyntaxNode<SyntaxKind>) -> Option<Self>;
fn syntax(&self) -> &SyntaxNode<SyntaxKind>;
fn kind(&self) -> SyntaxKind {
self.syntax().kind()
}
fn text_range(&self) -> TextRange {
self.syntax().text_range()
}
fn into_syntax(self) -> SyntaxNode<SyntaxKind>;
}
macro_rules! typed_cst_node {
($name:ident, $kind:expr) => {
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct $name {
syntax: SyntaxNode<SyntaxKind>,
}
impl $name {
pub const KIND: SyntaxKind = $kind;
}
impl TypedCstNode for $name {
fn cast(syntax: SyntaxNode<SyntaxKind>) -> Option<Self> {
(syntax.kind() == Self::KIND).then_some(Self { syntax })
}
fn syntax(&self) -> &SyntaxNode<SyntaxKind> {
&self.syntax
}
fn into_syntax(self) -> SyntaxNode<SyntaxKind> {
self.syntax
}
}
};
}
typed_cst_node!(StylesheetCstNode, SyntaxKind::Stylesheet);
typed_cst_node!(RuleCstNode, SyntaxKind::Rule);
typed_cst_node!(SelectorCstNode, SyntaxKind::Selector);
typed_cst_node!(DeclarationCstNode, SyntaxKind::Declaration);
typed_cst_node!(DeclarationListCstNode, SyntaxKind::DeclarationList);
typed_cst_node!(ValueCstNode, SyntaxKind::Value);
typed_cst_node!(ComponentValueCstNode, SyntaxKind::ComponentValue);
typed_cst_node!(SimpleBlockCstNode, SyntaxKind::SimpleBlock);
typed_cst_node!(ComponentValueListCstNode, SyntaxKind::ComponentValueList);
typed_cst_node!(
CommaSeparatedComponentValueListCstNode,
SyntaxKind::CommaSeparatedComponentValueList
);
typed_cst_node!(CustomPropertyValueCstNode, SyntaxKind::CustomPropertyValue);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AtRuleCstNode {
syntax: SyntaxNode<SyntaxKind>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BogusCstNode {
syntax: SyntaxNode<SyntaxKind>,
}
impl TypedCstNode for AtRuleCstNode {
fn cast(syntax: SyntaxNode<SyntaxKind>) -> Option<Self> {
is_at_rule_node_kind(syntax.kind()).then_some(Self { syntax })
}
fn syntax(&self) -> &SyntaxNode<SyntaxKind> {
&self.syntax
}
fn into_syntax(self) -> SyntaxNode<SyntaxKind> {
self.syntax
}
}
impl TypedCstNode for BogusCstNode {
fn cast(syntax: SyntaxNode<SyntaxKind>) -> Option<Self> {
syntax.kind().is_bogus().then_some(Self { syntax })
}
fn syntax(&self) -> &SyntaxNode<SyntaxKind> {
&self.syntax
}
fn into_syntax(self) -> SyntaxNode<SyntaxKind> {
self.syntax
}
}
pub fn is_at_rule_node_kind(kind: SyntaxKind) -> bool {
matches!(
kind,
SyntaxKind::AtRule
| SyntaxKind::MediaRule
| SyntaxKind::SupportsRule
| SyntaxKind::ContainerRule
| SyntaxKind::LayerRule
| SyntaxKind::ScopeRule
| SyntaxKind::KeyframesRule
| SyntaxKind::FontFaceRule
| SyntaxKind::PageRule
| SyntaxKind::NamespaceRule
| SyntaxKind::ImportRule
| SyntaxKind::CharsetRule
| SyntaxKind::PropertyRule
| SyntaxKind::StartingStyleRule
| SyntaxKind::PageMarginRule
| SyntaxKind::WhenRule
| SyntaxKind::ElseRule
| SyntaxKind::CounterStyleRule
| SyntaxKind::FontPaletteValuesRule
| SyntaxKind::ColorProfileRule
| SyntaxKind::PositionTryRule
| SyntaxKind::FontFeatureValuesRule
| SyntaxKind::FontFeatureValuesStylisticRule
| SyntaxKind::FontFeatureValuesStylesetRule
| SyntaxKind::FontFeatureValuesCharacterVariantRule
| SyntaxKind::FontFeatureValuesSwashRule
| SyntaxKind::FontFeatureValuesOrnamentsRule
| SyntaxKind::FontFeatureValuesAnnotationRule
| SyntaxKind::FontFeatureValuesHistoricalFormsRule
| SyntaxKind::ViewTransitionRule
| SyntaxKind::NestRule
| SyntaxKind::CustomMediaRule
| SyntaxKind::ScssUseRule
| SyntaxKind::ScssForwardRule
| SyntaxKind::ScssMixinDeclaration
| SyntaxKind::ScssIncludeRule
| SyntaxKind::ScssFunctionDeclaration
| SyntaxKind::ScssReturnRule
| SyntaxKind::ScssAtRootRule
| SyntaxKind::ScssErrorRule
| SyntaxKind::ScssWarnRule
| SyntaxKind::ScssDebugRule
| SyntaxKind::ScssContentRule
)
}
fn collect_typed_nodes<T>(
node: &SyntaxNode<SyntaxKind>,
cast: &impl Fn(SyntaxNode<SyntaxKind>) -> Option<T>,
nodes: &mut Vec<T>,
) {
if let Some(typed) = cast(node.clone()) {
nodes.push(typed);
}
for child in node.children() {
collect_typed_nodes(child, cast, nodes);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TokenSet {
kinds: &'static [SyntaxKind],
}
impl TokenSet {
pub const fn new(kinds: &'static [SyntaxKind]) -> Self {
Self { kinds }
}
pub fn contains(self, kind: SyntaxKind) -> bool {
self.kinds.contains(&kind)
}
pub fn len(self) -> usize {
self.kinds.len()
}
pub fn is_empty(self) -> bool {
self.kinds.is_empty()
}
}
pub const RECOVERY_TOP: TokenSet = TokenSet::new(&[
SyntaxKind::AtKeyword,
SyntaxKind::Dot,
SyntaxKind::Hash,
SyntaxKind::RightBrace,
SyntaxKind::Semicolon,
]);
pub const RECOVERY_DECLARATION: TokenSet =
TokenSet::new(&[SyntaxKind::Semicolon, SyntaxKind::RightBrace]);
pub const RECOVERY_SELECTOR: TokenSet = TokenSet::new(&[
SyntaxKind::Comma,
SyntaxKind::LeftBrace,
SyntaxKind::RightBrace,
]);
pub trait DialectExtension {
fn dialect(&self) -> StyleDialect;
fn classify_variable_token(&self, text: &str) -> Option<SyntaxKind> {
match self.dialect() {
StyleDialect::Css => None,
StyleDialect::Scss | StyleDialect::Sass if text.starts_with('$') => {
Some(SyntaxKind::ScssVariable)
}
StyleDialect::Less if text.starts_with('@') => Some(SyntaxKind::LessVariable),
StyleDialect::Scss | StyleDialect::Sass | StyleDialect::Less => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BuiltinDialectExtension {
dialect: StyleDialect,
}
impl BuiltinDialectExtension {
pub const fn new(dialect: StyleDialect) -> Self {
Self { dialect }
}
}
impl DialectExtension for BuiltinDialectExtension {
fn dialect(&self) -> StyleDialect {
self.dialect
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Token<'text> {
kind: SyntaxKind,
text: &'text str,
range: TextRange,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct AtRuleSpec {
node_kind: SyntaxKind,
block_kind: AtRuleBlockKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum AtRuleBlockKind {
GroupRuleList,
DeclarationList,
Keyframes,
Raw,
}
pub fn parse(text: &str, dialect: StyleDialect) -> ParseResult {
parse_entry_point(text, dialect, ParseEntryPoint::Stylesheet)
}
pub fn parse_entry_point(
text: &str,
dialect: StyleDialect,
entry_point: ParseEntryPoint,
) -> ParseResult {
let extension = BuiltinDialectExtension::new(dialect);
parse_entry_point_with_extension(text, &extension, entry_point)
}
pub fn lex(text: &str, dialect: StyleDialect) -> LexResult {
let extension = BuiltinDialectExtension::new(dialect);
lex_with_extension(text, &extension)
}
pub fn lex_with_extension(text: &str, extension: &impl DialectExtension) -> LexResult {
let (tokens, errors) = tokenize(text, extension);
LexResult {
tokens: tokens
.into_iter()
.map(|token| LexedToken {
kind: token.kind,
range: token.range,
text: public_token_text(token.text),
})
.collect(),
errors,
dialect: extension.dialect(),
}
}
pub fn parse_with_extension(text: &str, extension: &impl DialectExtension) -> ParseResult {
parse_entry_point_with_extension(text, extension, ParseEntryPoint::Stylesheet)
}
pub fn parse_entry_point_with_extension(
text: &str,
extension: &impl DialectExtension,
entry_point: ParseEntryPoint,
) -> ParseResult {
let (tokens, errors) = tokenize(text, extension);
let token_count = tokens.len();
let mut parser = Parser::new(tokens, errors, extension.dialect());
let (green, interner) = parser.parse_entry_point(entry_point);
ParseResult {
green,
interner,
errors: parser.into_errors(),
token_count,
dialect: extension.dialect(),
}
}
pub fn collect_style_facts(text: &str, dialect: StyleDialect) -> ParsedStyleFacts {
let extension = BuiltinDialectExtension::new(dialect);
collect_style_facts_with_extension(text, &extension)
}
pub fn summarize_omena_parser_style_facts(
style_source: &str,
dialect: StyleDialect,
) -> OmenaParserStyleFactsSummaryV0 {
let facts = collect_style_facts(style_source, dialect);
let sass_symbol_resolution = summarize_omena_parser_sass_symbol_resolution(&facts.sass_symbols);
let mut class_selector_names = Vec::new();
let mut id_selector_names = Vec::new();
let mut placeholder_selector_names = Vec::new();
let mut keyframe_names = Vec::new();
let mut animation_reference_names = Vec::new();
let mut css_module_value_definition_names = BTreeSet::new();
let mut css_module_value_reference_names = BTreeSet::new();
let mut css_module_value_import_sources = BTreeSet::new();
let mut css_module_composes_target_names = BTreeSet::new();
let mut css_module_composes_import_sources = BTreeSet::new();
let mut icss_export_names = BTreeSet::new();
let mut icss_import_local_names = BTreeSet::new();
let mut icss_import_remote_names = BTreeSet::new();
let mut icss_import_sources = BTreeSet::new();
let mut variable_names = BTreeSet::new();
let mut sass_symbol_declaration_names = BTreeSet::new();
let mut sass_symbol_reference_names = BTreeSet::new();
let mut sass_module_use_sources = BTreeSet::new();
let mut sass_module_forward_sources = BTreeSet::new();
let mut sass_module_import_sources = BTreeSet::new();
let mut custom_property_names = BTreeSet::new();
let mut custom_property_decl_names = BTreeSet::new();
let mut custom_property_ref_names = BTreeSet::new();
for selector in facts.selectors {
match selector.kind {
ParsedSelectorFactKind::Class => class_selector_names.push(selector.name),
ParsedSelectorFactKind::Id => id_selector_names.push(selector.name),
ParsedSelectorFactKind::Placeholder => placeholder_selector_names.push(selector.name),
}
}
for variable in facts.variables {
match variable.kind {
ParsedVariableFactKind::ScssDeclaration
| ParsedVariableFactKind::ScssReference
| ParsedVariableFactKind::LessDeclaration
| ParsedVariableFactKind::LessReference => {
variable_names.insert(variable.name);
}
ParsedVariableFactKind::CustomPropertyDeclaration
| ParsedVariableFactKind::CustomPropertyReference => {
custom_property_names.insert(variable.name.clone());
match variable.kind {
ParsedVariableFactKind::CustomPropertyDeclaration => {
custom_property_decl_names.insert(variable.name);
}
ParsedVariableFactKind::CustomPropertyReference => {
custom_property_ref_names.insert(variable.name);
}
_ => {}
}
}
}
}
for symbol in &facts.sass_symbols {
match symbol.role {
"declaration" => {
sass_symbol_declaration_names.insert(symbol.name.clone());
}
_ => {
sass_symbol_reference_names.insert(symbol.name.clone());
}
}
}
for edge in &facts.sass_module_edges {
match edge.kind {
ParsedSassModuleEdgeFactKind::Use => {
sass_module_use_sources.insert(edge.source.clone());
}
ParsedSassModuleEdgeFactKind::Forward => {
sass_module_forward_sources.insert(edge.source.clone());
}
ParsedSassModuleEdgeFactKind::Import => {
sass_module_import_sources.insert(edge.source.clone());
}
}
}
for animation in facts.animations {
match animation.kind {
ParsedAnimationFactKind::KeyframesDeclaration => keyframe_names.push(animation.name),
ParsedAnimationFactKind::AnimationNameReference => {
animation_reference_names.push(animation.name);
}
}
}
for value in facts.css_module_values {
match value.kind {
ParsedCssModuleValueFactKind::Definition => {
css_module_value_definition_names.insert(value.name);
}
ParsedCssModuleValueFactKind::Reference => {
css_module_value_reference_names.insert(value.name);
}
ParsedCssModuleValueFactKind::ImportSource => {
css_module_value_import_sources.insert(value.name);
}
}
}
for composes in facts.css_module_composes {
match composes.kind {
ParsedCssModuleComposesFactKind::Target => {
css_module_composes_target_names.insert(composes.name);
}
ParsedCssModuleComposesFactKind::ImportSource => {
css_module_composes_import_sources.insert(composes.name);
}
}
}
for icss in facts.icss {
match icss.kind {
ParsedIcssFactKind::ExportName => {
icss_export_names.insert(icss.name);
}
ParsedIcssFactKind::ImportLocalName => {
icss_import_local_names.insert(icss.name);
}
ParsedIcssFactKind::ImportRemoteName => {
icss_import_remote_names.insert(icss.name);
}
ParsedIcssFactKind::ImportSource => {
icss_import_sources.insert(icss.name);
}
}
}
OmenaParserStyleFactsSummaryV0 {
schema_version: "0",
product: "omena-parser.style-facts",
dialect: style_dialect_label(dialect),
class_selector_names,
id_selector_names,
placeholder_selector_names,
keyframe_names,
animation_reference_names,
css_module_value_definition_names: css_module_value_definition_names.into_iter().collect(),
css_module_value_reference_names: css_module_value_reference_names.into_iter().collect(),
css_module_value_import_sources: css_module_value_import_sources.into_iter().collect(),
css_module_value_import_edges: facts
.css_module_value_import_edges
.into_iter()
.map(|edge| OmenaParserCssModuleValueImportEdgeFactV0 {
remote_name: edge.remote_name,
local_name: edge.local_name,
import_source: edge.import_source,
})
.collect(),
css_module_value_definition_edges: facts
.css_module_value_definition_edges
.into_iter()
.map(|edge| OmenaParserCssModuleValueDefinitionEdgeFactV0 {
definition_name: edge.definition_name,
reference_names: edge.reference_names,
})
.collect(),
css_module_composes_target_names: css_module_composes_target_names.into_iter().collect(),
css_module_composes_import_sources: css_module_composes_import_sources
.into_iter()
.collect(),
css_module_composes_edges: facts
.css_module_composes_edges
.into_iter()
.map(|edge| OmenaParserCssModuleComposesEdgeFactV0 {
kind: css_module_composes_edge_kind_label(edge.kind),
owner_selector_names: edge.owner_selector_names,
target_names: edge.target_names,
import_source: edge.import_source,
})
.collect(),
icss_export_names: icss_export_names.into_iter().collect(),
icss_import_local_names: icss_import_local_names.into_iter().collect(),
icss_import_remote_names: icss_import_remote_names.into_iter().collect(),
icss_import_sources: icss_import_sources.into_iter().collect(),
icss_import_edges: facts
.icss_import_edges
.into_iter()
.map(|edge| OmenaParserIcssImportEdgeFactV0 {
local_name: edge.local_name,
remote_name: edge.remote_name,
import_source: edge.import_source,
})
.collect(),
icss_export_edges: facts
.icss_export_edges
.into_iter()
.map(|edge| OmenaParserIcssExportEdgeFactV0 {
export_name: edge.export_name,
reference_names: edge.reference_names,
})
.collect(),
variable_names: variable_names.into_iter().collect(),
sass_symbol_declaration_names: sass_symbol_declaration_names.into_iter().collect(),
sass_symbol_reference_names: sass_symbol_reference_names.into_iter().collect(),
sass_symbol_facts: facts
.sass_symbols
.into_iter()
.map(|symbol| OmenaParserSassSymbolFactV0 {
kind: sass_symbol_fact_kind_label(symbol.kind),
symbol_kind: symbol.symbol_kind,
name: symbol.name,
role: symbol.role,
namespace: symbol.namespace,
})
.collect(),
sass_symbol_resolution,
sass_module_use_sources: sass_module_use_sources.into_iter().collect(),
sass_module_forward_sources: sass_module_forward_sources.into_iter().collect(),
sass_module_import_sources: sass_module_import_sources.into_iter().collect(),
sass_module_edges: facts
.sass_module_edges
.into_iter()
.map(|edge| OmenaParserSassModuleEdgeFactV0 {
kind: sass_module_edge_fact_kind_label(edge.kind),
source: edge.source,
namespace_kind: edge.namespace_kind,
namespace: edge.namespace,
visibility_filter_kind: edge.visibility_filter_kind,
visibility_filter_names: edge.visibility_filter_names,
})
.collect(),
custom_property_names: custom_property_names.into_iter().collect(),
custom_property_decl_names: custom_property_decl_names.into_iter().collect(),
custom_property_ref_names: custom_property_ref_names.into_iter().collect(),
at_rule_names: facts
.at_rules
.into_iter()
.map(|at_rule| at_rule.name)
.collect(),
parser_error_count: facts.error_count,
}
}
pub fn summarize_omena_parser_lex(source: &str, dialect: StyleDialect) -> OmenaParserLexSummaryV0 {
let result = lex(source, dialect);
OmenaParserLexSummaryV0 {
schema_version: "0",
product: "omena-parser.lex-result",
dialect: style_dialect_label(result.dialect()),
tokens: result
.tokens()
.iter()
.map(|token| OmenaParserLexTokenV0 {
kind: format!("{:?}", token.kind),
text: token.text.clone(),
start: token.range.start().into(),
end: token.range.end().into(),
})
.collect(),
parser_error_count: result.errors().len(),
}
}
pub fn summarize_omena_parser_parity_lite(
source: &str,
dialect: StyleDialect,
) -> OmenaParserParityLiteSummaryV0 {
let facts = collect_style_facts(source, dialect);
let result = parse(source, dialect);
let (tokens, _) = tokenize(source, &BuiltinDialectExtension::new(dialect));
let mut structural = ParserStructuralSummary::default();
summarize_parser_structural_range(&tokens, 0, tokens.len(), 0, &mut structural);
let mut selector_names = collect_parity_lite_selector_names_from_tokens(&tokens);
selector_names.sort();
OmenaParserParityLiteSummaryV0 {
schema_version: "0",
language: style_dialect_label(dialect),
selector_names,
keyframes_names: sorted_unique(
facts
.animations
.iter()
.filter(|animation| animation.kind == ParsedAnimationFactKind::KeyframesDeclaration)
.map(|animation| animation.name.clone()),
),
value_decl_names: sorted_unique(
facts
.css_module_values
.iter()
.filter(|value| value.kind == ParsedCssModuleValueFactKind::Definition)
.map(|value| value.name.clone()),
),
diagnostic_count: result.errors().len(),
rule_count: structural.rule_count,
declaration_count: structural.declaration_count,
grouped_selector_count: structural.grouped_selector_count,
max_nesting_depth: structural.max_nesting_depth,
at_rule_kind_counts: structural.at_rule_kind_counts,
declaration_kind_counts: structural.declaration_kind_counts,
}
}
fn style_dialect_label(dialect: StyleDialect) -> &'static str {
match dialect {
StyleDialect::Css => "css",
StyleDialect::Scss => "scss",
StyleDialect::Sass => "sass",
StyleDialect::Less => "less",
}
}
#[derive(Default)]
struct ParserStructuralSummary {
rule_count: usize,
declaration_count: usize,
grouped_selector_count: usize,
max_nesting_depth: usize,
at_rule_kind_counts: OmenaParserAtRuleKindCountsV0,
declaration_kind_counts: OmenaParserDeclarationKindCountsV0,
}
fn summarize_parser_structural_range(
tokens: &[Token<'_>],
start: usize,
end: usize,
depth: usize,
summary: &mut ParserStructuralSummary,
) {
let mut index = start;
while index < end {
index = skip_trivia_tokens(tokens, index, end);
if index >= end {
break;
}
if tokens[index].kind == SyntaxKind::AtKeyword {
increment_omena_parser_at_rule_kind_count(
&mut summary.at_rule_kind_counts,
classify_omena_parser_at_rule_kind(tokens[index].text),
);
let next_depth = depth + 1;
summary.max_nesting_depth = summary.max_nesting_depth.max(next_depth);
if let Some((open, close)) = find_block_after_header(tokens, index, end) {
summarize_parser_structural_range(tokens, open + 1, close, next_depth, summary);
index = close + 1;
} else {
index = skip_statement(tokens, index, end);
}
continue;
}
let statement_end = css_module_value_statement_end(tokens, index);
if is_root_less_variable_statement(tokens, index, statement_end.min(end), depth) {
increment_omena_parser_at_rule_kind_count(
&mut summary.at_rule_kind_counts,
keyof_omena_parser_at_rule_kind_counts::Kind::Generic,
);
if statement_end >= end || tokens[statement_end].kind == SyntaxKind::RightBrace {
break;
}
index = statement_end + 1;
continue;
}
if statement_end < end && tokens[statement_end].kind == SyntaxKind::LeftBrace {
summary.rule_count += 1;
let next_depth = depth + 1;
summary.max_nesting_depth = summary.max_nesting_depth.max(next_depth);
let group_count = count_omena_parser_selector_groups(tokens, index, statement_end);
if group_count > 1 {
summary.grouped_selector_count += group_count;
}
if let Some(close) = matching_right_brace(tokens, statement_end, end) {
summarize_parser_structural_range(
tokens,
statement_end + 1,
close,
next_depth,
summary,
);
index = close + 1;
} else {
index = statement_end + 1;
}
continue;
}
if let Some(colon_index) = declaration_colon_index(tokens, index, statement_end.min(end)) {
summary.declaration_count += 1;
let property = previous_non_trivia_token_index(tokens, colon_index, index)
.map(|property| tokens[property].text)
.unwrap_or_default();
increment_omena_parser_declaration_kind_count(
&mut summary.declaration_kind_counts,
classify_omena_parser_declaration_kind(property),
);
}
if statement_end >= end || tokens[statement_end].kind == SyntaxKind::RightBrace {
break;
}
index = statement_end + 1;
}
}
fn is_root_less_variable_statement(
tokens: &[Token<'_>],
start: usize,
end: usize,
depth: usize,
) -> bool {
if depth != 0 {
return false;
}
let Some(first) = next_non_trivia_token_index_until(tokens, start, end) else {
return false;
};
tokens[first].kind == SyntaxKind::LessVariable
&& declaration_colon_index(tokens, first, end).is_some()
}
fn count_omena_parser_selector_groups(tokens: &[Token<'_>], start: usize, end: usize) -> usize {
split_selector_groups(tokens, start, end)
.into_iter()
.filter(|(group_start, group_end)| {
*group_start < *group_end
&& next_non_trivia_token_index_until(tokens, *group_start, *group_end).is_some()
})
.count()
}
fn collect_parity_lite_selector_names_from_tokens(tokens: &[Token<'_>]) -> Vec<String> {
let mut names = Vec::new();
collect_parity_lite_selector_names_in_range(tokens, 0, tokens.len(), &[], None, &mut names);
names
}
fn collect_parity_lite_selector_names_in_range(
tokens: &[Token<'_>],
start: usize,
end: usize,
parent_branches: &[SelectorBranch],
css_module_scope: Option<&'static str>,
names: &mut Vec<String>,
) {
let mut index = start;
while index < end {
index = skip_trivia_tokens(tokens, index, end);
if index >= end {
break;
}
if tokens[index].kind == SyntaxKind::AtKeyword {
let block = find_block_after_header(tokens, index, end);
if let Some((open, close)) = block {
if tokens[index].text == "@nest" {
if css_module_scope == Some("global") {
collect_parity_lite_selector_names_in_range(
tokens,
open + 1,
close,
&[],
css_module_scope,
names,
);
} else {
let branches =
resolve_selector_header(tokens, index + 1, open, parent_branches);
names.extend(branches.iter().map(|branch| branch.name.clone()));
collect_grouped_ampersand_compound_selector_duplicates(
tokens,
index + 1,
open,
parent_branches.len(),
names,
);
collect_parity_lite_selector_names_in_range(
tokens,
open + 1,
close,
&branches,
css_module_scope,
names,
);
}
} else if style_wrapper_at_rule(tokens[index].text) {
collect_parity_lite_selector_names_in_range(
tokens,
open + 1,
close,
parent_branches,
css_module_scope,
names,
);
}
index = close + 1;
} else {
index = skip_statement(tokens, index, end);
}
continue;
}
let Some((open, close)) = find_block_after_header(tokens, index, end) else {
index = skip_statement(tokens, index, end);
continue;
};
let effective_scope = css_module_scope
.or_else(|| css_module_block_scope_marker_in_header(tokens, index, open));
if effective_scope == Some("global") {
collect_parity_lite_selector_names_in_range(
tokens,
open + 1,
close,
&[],
effective_scope,
names,
);
} else {
let branches = resolve_selector_header(tokens, index, open, parent_branches);
names.extend(branches.iter().map(|branch| branch.name.clone()));
collect_grouped_ampersand_compound_selector_duplicates(
tokens,
index,
open,
parent_branches.len(),
names,
);
collect_parity_lite_selector_names_in_range(
tokens,
open + 1,
close,
&branches,
effective_scope,
names,
);
}
index = close + 1;
}
}
fn collect_grouped_ampersand_compound_selector_duplicates(
tokens: &[Token<'_>],
start: usize,
end: usize,
parent_branch_count: usize,
names: &mut Vec<String>,
) {
if parent_branch_count <= 1 || !header_contains_ampersand(tokens, start, end) {
return;
}
for (name, _) in collect_class_selector_names_from_header(tokens, start, end) {
names.extend(std::iter::repeat_n(name, parent_branch_count - 1));
}
}
fn header_contains_ampersand(tokens: &[Token<'_>], start: usize, end: usize) -> bool {
tokens[start..end]
.iter()
.any(|token| token.kind == SyntaxKind::Ampersand)
}
fn classify_omena_parser_at_rule_kind(text: &str) -> keyof_omena_parser_at_rule_kind_counts::Kind {
match text.trim_start_matches('@').to_ascii_lowercase().as_str() {
"media" => keyof_omena_parser_at_rule_kind_counts::Kind::Media,
"supports" => keyof_omena_parser_at_rule_kind_counts::Kind::Supports,
"layer" => keyof_omena_parser_at_rule_kind_counts::Kind::Layer,
"keyframes" | "-webkit-keyframes" => {
keyof_omena_parser_at_rule_kind_counts::Kind::Keyframes
}
"value" => keyof_omena_parser_at_rule_kind_counts::Kind::Value,
"at-root" => keyof_omena_parser_at_rule_kind_counts::Kind::AtRoot,
_ => keyof_omena_parser_at_rule_kind_counts::Kind::Generic,
}
}
fn increment_omena_parser_at_rule_kind_count(
counts: &mut OmenaParserAtRuleKindCountsV0,
kind: keyof_omena_parser_at_rule_kind_counts::Kind,
) {
match kind {
keyof_omena_parser_at_rule_kind_counts::Kind::Media => counts.media += 1,
keyof_omena_parser_at_rule_kind_counts::Kind::Supports => counts.supports += 1,
keyof_omena_parser_at_rule_kind_counts::Kind::Layer => counts.layer += 1,
keyof_omena_parser_at_rule_kind_counts::Kind::Keyframes => counts.keyframes += 1,
keyof_omena_parser_at_rule_kind_counts::Kind::Value => counts.value += 1,
keyof_omena_parser_at_rule_kind_counts::Kind::AtRoot => counts.at_root += 1,
keyof_omena_parser_at_rule_kind_counts::Kind::Generic => counts.generic += 1,
}
}
fn classify_omena_parser_declaration_kind(
property: &str,
) -> keyof_omena_parser_declaration_kind_counts::Kind {
match property.trim().to_ascii_lowercase().as_str() {
"composes" => keyof_omena_parser_declaration_kind_counts::Kind::Composes,
"animation" => keyof_omena_parser_declaration_kind_counts::Kind::Animation,
"animation-name" => keyof_omena_parser_declaration_kind_counts::Kind::AnimationName,
_ => keyof_omena_parser_declaration_kind_counts::Kind::Generic,
}
}
fn increment_omena_parser_declaration_kind_count(
counts: &mut OmenaParserDeclarationKindCountsV0,
kind: keyof_omena_parser_declaration_kind_counts::Kind,
) {
match kind {
keyof_omena_parser_declaration_kind_counts::Kind::Composes => counts.composes += 1,
keyof_omena_parser_declaration_kind_counts::Kind::Animation => counts.animation += 1,
keyof_omena_parser_declaration_kind_counts::Kind::AnimationName => {
counts.animation_name += 1
}
keyof_omena_parser_declaration_kind_counts::Kind::Generic => counts.generic += 1,
}
}
mod keyof_omena_parser_at_rule_kind_counts {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Kind {
Media,
Supports,
Layer,
Keyframes,
Value,
AtRoot,
Generic,
}
}
mod keyof_omena_parser_declaration_kind_counts {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Kind {
Composes,
Animation,
AnimationName,
Generic,
}
}
fn sorted_unique(values: impl IntoIterator<Item = String>) -> Vec<String> {
values
.into_iter()
.collect::<BTreeSet<_>>()
.into_iter()
.collect()
}
fn css_module_composes_edge_kind_label(kind: ParsedCssModuleComposesEdgeKind) -> &'static str {
match kind {
ParsedCssModuleComposesEdgeKind::Local => "local",
ParsedCssModuleComposesEdgeKind::Global => "global",
ParsedCssModuleComposesEdgeKind::External => "external",
}
}
fn sass_symbol_fact_kind_label(kind: ParsedSassSymbolFactKind) -> &'static str {
match kind {
ParsedSassSymbolFactKind::VariableDeclaration => "sassVariableDeclaration",
ParsedSassSymbolFactKind::VariableReference => "sassVariableReference",
ParsedSassSymbolFactKind::MixinDeclaration => "sassMixinDeclaration",
ParsedSassSymbolFactKind::MixinInclude => "sassMixinInclude",
ParsedSassSymbolFactKind::FunctionDeclaration => "sassFunctionDeclaration",
ParsedSassSymbolFactKind::FunctionCall => "sassFunctionCall",
}
}
fn sass_module_edge_fact_kind_label(kind: ParsedSassModuleEdgeFactKind) -> &'static str {
match kind {
ParsedSassModuleEdgeFactKind::Use => "sassUse",
ParsedSassModuleEdgeFactKind::Forward => "sassForward",
ParsedSassModuleEdgeFactKind::Import => "sassImport",
}
}
fn summarize_omena_parser_sass_symbol_resolution(
symbols: &[ParsedSassSymbolFact],
) -> OmenaParserSassSymbolResolutionV0 {
let mut declaration_by_symbol: BTreeMap<
(&'static str, Option<String>, String),
(usize, &'static str),
> = BTreeMap::new();
let mut declaration_count = 0usize;
let mut reference_count = 0usize;
let mut edges = Vec::new();
for (source_order, symbol) in symbols.iter().enumerate() {
let kind = sass_symbol_fact_kind_label(symbol.kind);
if sass_symbol_fact_kind_is_declaration(symbol.kind) {
declaration_count += 1;
declaration_by_symbol.insert(
(
symbol.symbol_kind,
symbol.namespace.clone(),
symbol.name.clone(),
),
(source_order, kind),
);
continue;
}
if !sass_symbol_fact_kind_is_reference(symbol.kind) {
continue;
}
reference_count += 1;
let declaration = declaration_by_symbol.get(&(
symbol.symbol_kind,
symbol.namespace.clone(),
symbol.name.clone(),
));
edges.push(OmenaParserSassSymbolResolutionEdgeV0 {
symbol_kind: symbol.symbol_kind,
name: symbol.name.clone(),
namespace: symbol.namespace.clone(),
reference_kind: kind,
reference_role: symbol.role,
reference_source_order: source_order,
declaration_kind: declaration.map(|(_, declaration_kind)| *declaration_kind),
declaration_source_order: declaration.map(|(declaration_order, _)| *declaration_order),
status: if declaration.is_some() {
"resolved"
} else {
"unresolved"
},
});
}
let resolved_reference_count = edges
.iter()
.filter(|edge| edge.status == "resolved")
.count();
OmenaParserSassSymbolResolutionV0 {
schema_version: "0",
product: "omena-parser.sass-symbol-same-file-resolution",
resolution_scope: "same-file",
declaration_count,
reference_count,
resolved_reference_count,
unresolved_reference_count: reference_count.saturating_sub(resolved_reference_count),
edges,
capabilities: OmenaParserSassSymbolResolutionCapabilitiesV0 {
same_file_lexical_resolution_ready: true,
declaration_before_reference_ready: true,
unresolved_reference_reporting_ready: true,
cross_file_module_resolution_ready: false,
},
}
}
fn sass_symbol_fact_kind_is_declaration(kind: ParsedSassSymbolFactKind) -> bool {
matches!(
kind,
ParsedSassSymbolFactKind::VariableDeclaration
| ParsedSassSymbolFactKind::MixinDeclaration
| ParsedSassSymbolFactKind::FunctionDeclaration
)
}
fn sass_symbol_fact_kind_is_reference(kind: ParsedSassSymbolFactKind) -> bool {
matches!(
kind,
ParsedSassSymbolFactKind::VariableReference
| ParsedSassSymbolFactKind::MixinInclude
| ParsedSassSymbolFactKind::FunctionCall
)
}
pub fn summarize_parser_cst_equivalence(
text: &str,
dialect: StyleDialect,
) -> ParserCstEquivalenceSummaryV0 {
let result = parse(text, dialect);
let syntax = result.syntax();
let cst = result.cst();
let mut node_count = 0;
let mut token_count = 0;
let mut syntax_kind_round_trip_ready = true;
let mut zero_unknown_kind_ready = true;
for node in syntax.descendants() {
node_count += 1;
let kind = node.kind();
syntax_kind_round_trip_ready &= SyntaxKind::from_raw(kind.into_raw()) == kind;
zero_unknown_kind_ready &= SyntaxKind::ALL.contains(&kind);
}
for token in syntax
.descendants_with_tokens()
.filter_map(|element| element.into_token())
{
token_count += 1;
let kind = token.kind();
syntax_kind_round_trip_ready &= SyntaxKind::from_raw(kind.into_raw()) == kind;
zero_unknown_kind_ready &= SyntaxKind::ALL.contains(&kind);
}
let typed_wrapper_count = usize::from(cst.stylesheet().is_some())
+ cst.rules().len()
+ cst.selectors().len()
+ cst.declarations().len()
+ cst.declaration_lists().len()
+ cst.values().len()
+ cst.component_values().len()
+ cst.simple_blocks().len()
+ cst.component_value_lists().len()
+ cst.comma_separated_component_value_lists().len()
+ cst.custom_property_values().len()
+ cst.at_rules().len()
+ cst.bogus_nodes().len();
ParserCstEquivalenceSummaryV0 {
product: "omena-parser.cst-equivalence",
dialect,
root_kind: syntax.kind(),
parser_node_count: node_count,
parser_token_count: token_count,
typed_wrapper_count,
source_text_round_trip_ready: result.source_text().as_deref() == Some(text),
syntax_kind_round_trip_ready,
zero_unknown_kind_ready,
typed_cst_wrapper_ready: cst.stylesheet().is_some() && typed_wrapper_count > 1,
ready_surfaces: vec![
"parserCstEquivalence",
"parserUsesOmenaSyntaxKind",
"parserCstSourceTextRoundTrip",
"typedCstWrapperEquivalence",
],
}
}
pub fn collect_style_facts_with_extension(
text: &str,
extension: &impl DialectExtension,
) -> ParsedStyleFacts {
let (tokens, lex_errors) = tokenize(text, extension);
let mut parser = Parser::new(tokens.clone(), lex_errors, extension.dialect());
let _green = parser.parse();
let errors = parser.into_errors();
let selectors = collect_selector_facts_from_tokens(&tokens);
let variables = collect_variable_facts_from_tokens(&tokens);
let sass_symbols = collect_sass_symbol_facts_from_tokens(&tokens);
let sass_includes = collect_sass_include_facts_from_tokens(text, &tokens);
let sass_module_edges = collect_sass_module_edge_facts_from_tokens(&tokens);
let animations = collect_animation_facts_from_tokens(&tokens);
let css_module_values = collect_css_module_value_facts_from_tokens(&tokens);
let css_module_value_import_edges =
collect_css_module_value_import_edge_facts_from_tokens(&tokens);
let css_module_value_definition_edges =
collect_css_module_value_definition_edge_facts_from_tokens(&tokens);
let css_module_composes = collect_css_module_composes_facts_from_tokens(&tokens);
let css_module_composes_edges = collect_css_module_composes_edge_facts_from_tokens(&tokens);
let icss = collect_icss_facts_from_tokens(&tokens);
let icss_import_edges = collect_icss_import_edge_facts_from_tokens(&tokens);
let icss_export_edges = collect_icss_export_edge_facts_from_tokens(&tokens);
let at_rules = collect_at_rule_facts_from_tokens(&tokens, extension.dialect());
ParsedStyleFacts {
product: "omena-parser.style-facts",
dialect: extension.dialect(),
selector_count: selectors.len(),
selectors,
variable_count: variables.len(),
variables,
sass_symbol_count: sass_symbols.len(),
sass_symbols,
sass_include_count: sass_includes.len(),
sass_includes,
sass_module_edge_count: sass_module_edges.len(),
sass_module_edges,
animation_count: animations.len(),
animations,
css_module_value_count: css_module_values.len(),
css_module_values,
css_module_value_import_edge_count: css_module_value_import_edges.len(),
css_module_value_import_edges,
css_module_value_definition_edge_count: css_module_value_definition_edges.len(),
css_module_value_definition_edges,
css_module_composes_count: css_module_composes.len(),
css_module_composes,
css_module_composes_edge_count: css_module_composes_edges.len(),
css_module_composes_edges,
icss_count: icss.len(),
icss,
icss_import_edge_count: icss_import_edges.len(),
icss_import_edges,
icss_export_edge_count: icss_export_edges.len(),
icss_export_edges,
at_rule_count: at_rules.len(),
at_rules,
error_count: errors.len(),
}
}
pub fn summarize_parser_semantic_name_consumption(
text: &str,
dialect: StyleDialect,
db: &dyn salsa::Database,
) -> ParserSemanticNameConsumptionSummaryV0 {
let facts = collect_style_facts(text, dialect);
let candidates = parser_semantic_name_candidates(&facts);
let interned_name_count = candidates
.iter()
.filter(|candidate| intern_parser_semantic_name(db, candidate.kind, &candidate.text))
.count();
let invalid_name_count = candidates.len().saturating_sub(interned_name_count);
ParserSemanticNameConsumptionSummaryV0 {
product: "omena-parser.semantic-name-consumption",
dialect,
semantic_name_count: candidates.len(),
interned_name_count,
invalid_name_count,
class_name_count: count_parser_semantic_name_kind(&candidates, NameKind::ClassName),
css_ident_count: count_parser_semantic_name_kind(&candidates, NameKind::CssIdent),
property_name_count: count_parser_semantic_name_kind(&candidates, NameKind::PropertyName),
selector_key_count: count_parser_semantic_name_kind(&candidates, NameKind::SelectorKey),
custom_property_name_count: count_parser_semantic_name_kind(
&candidates,
NameKind::CustomPropertyName,
),
keyframes_name_count: count_parser_semantic_name_kind(&candidates, NameKind::KeyframesName),
mixin_name_count: count_parser_semantic_name_kind(&candidates, NameKind::MixinName),
file_path_count: count_parser_semantic_name_kind(&candidates, NameKind::FilePath),
ready_surfaces: vec![
"parserSemanticNameConsumption",
"typedInternerValidation",
"styleFactNameKindProjection",
],
}
}
fn parser_semantic_name_candidates(facts: &ParsedStyleFacts) -> Vec<ParserSemanticNameCandidateV0> {
let mut candidates = Vec::new();
for selector in &facts.selectors {
let kind = match selector.kind {
ParsedSelectorFactKind::Class => NameKind::ClassName,
ParsedSelectorFactKind::Id | ParsedSelectorFactKind::Placeholder => {
NameKind::SelectorKey
}
};
push_parser_semantic_name_candidate(&mut candidates, kind, &selector.name);
}
for variable in &facts.variables {
let kind = match variable.kind {
ParsedVariableFactKind::CustomPropertyDeclaration
| ParsedVariableFactKind::CustomPropertyReference => NameKind::CustomPropertyName,
ParsedVariableFactKind::ScssDeclaration
| ParsedVariableFactKind::ScssReference
| ParsedVariableFactKind::LessDeclaration
| ParsedVariableFactKind::LessReference => NameKind::CssIdent,
};
push_parser_semantic_name_candidate(&mut candidates, kind, &variable.name);
}
for symbol in &facts.sass_symbols {
let kind = match symbol.kind {
ParsedSassSymbolFactKind::MixinDeclaration | ParsedSassSymbolFactKind::MixinInclude => {
NameKind::MixinName
}
ParsedSassSymbolFactKind::VariableDeclaration
| ParsedSassSymbolFactKind::VariableReference
| ParsedSassSymbolFactKind::FunctionDeclaration
| ParsedSassSymbolFactKind::FunctionCall => NameKind::CssIdent,
};
push_parser_semantic_name_candidate(&mut candidates, kind, &symbol.name);
if let Some(namespace) = &symbol.namespace {
push_parser_semantic_name_candidate(&mut candidates, NameKind::CssIdent, namespace);
}
}
for include in &facts.sass_includes {
push_parser_semantic_name_candidate(&mut candidates, NameKind::MixinName, &include.name);
if let Some(namespace) = &include.namespace {
push_parser_semantic_name_candidate(&mut candidates, NameKind::CssIdent, namespace);
}
}
for edge in &facts.sass_module_edges {
push_parser_semantic_name_candidate(&mut candidates, NameKind::FilePath, &edge.source);
if let Some(namespace) = &edge.namespace {
push_parser_semantic_name_candidate(&mut candidates, NameKind::CssIdent, namespace);
}
}
for animation in &facts.animations {
push_parser_semantic_name_candidate(
&mut candidates,
NameKind::KeyframesName,
&animation.name,
);
}
for value in &facts.css_module_values {
let kind = match value.kind {
ParsedCssModuleValueFactKind::Definition | ParsedCssModuleValueFactKind::Reference => {
NameKind::CssIdent
}
ParsedCssModuleValueFactKind::ImportSource => NameKind::FilePath,
};
push_parser_semantic_name_candidate(&mut candidates, kind, &value.name);
}
for edge in &facts.css_module_value_import_edges {
push_parser_semantic_name_candidate(&mut candidates, NameKind::CssIdent, &edge.local_name);
push_parser_semantic_name_candidate(&mut candidates, NameKind::CssIdent, &edge.remote_name);
push_parser_semantic_name_candidate(
&mut candidates,
NameKind::FilePath,
&edge.import_source,
);
}
for edge in &facts.css_module_value_definition_edges {
push_parser_semantic_name_candidate(
&mut candidates,
NameKind::CssIdent,
&edge.definition_name,
);
for reference_name in &edge.reference_names {
push_parser_semantic_name_candidate(
&mut candidates,
NameKind::CssIdent,
reference_name,
);
}
}
for composes in &facts.css_module_composes {
let kind = match composes.kind {
ParsedCssModuleComposesFactKind::Target => NameKind::ClassName,
ParsedCssModuleComposesFactKind::ImportSource => NameKind::FilePath,
};
push_parser_semantic_name_candidate(&mut candidates, kind, &composes.name);
}
for edge in &facts.css_module_composes_edges {
for owner_selector_name in &edge.owner_selector_names {
push_parser_semantic_name_candidate(
&mut candidates,
NameKind::ClassName,
owner_selector_name,
);
}
for target_name in &edge.target_names {
push_parser_semantic_name_candidate(&mut candidates, NameKind::ClassName, target_name);
}
if let Some(import_source) = &edge.import_source {
push_parser_semantic_name_candidate(&mut candidates, NameKind::FilePath, import_source);
}
}
for icss in &facts.icss {
let kind = match icss.kind {
ParsedIcssFactKind::ImportSource => NameKind::FilePath,
ParsedIcssFactKind::ExportName
| ParsedIcssFactKind::ImportLocalName
| ParsedIcssFactKind::ImportRemoteName => NameKind::CssIdent,
};
push_parser_semantic_name_candidate(&mut candidates, kind, &icss.name);
}
for edge in &facts.icss_import_edges {
push_parser_semantic_name_candidate(&mut candidates, NameKind::CssIdent, &edge.local_name);
push_parser_semantic_name_candidate(&mut candidates, NameKind::CssIdent, &edge.remote_name);
push_parser_semantic_name_candidate(
&mut candidates,
NameKind::FilePath,
&edge.import_source,
);
}
for edge in &facts.icss_export_edges {
push_parser_semantic_name_candidate(&mut candidates, NameKind::CssIdent, &edge.export_name);
for reference_name in &edge.reference_names {
push_parser_semantic_name_candidate(
&mut candidates,
NameKind::CssIdent,
reference_name,
);
}
}
for at_rule in &facts.at_rules {
push_parser_semantic_name_candidate(&mut candidates, NameKind::CssIdent, &at_rule.name);
}
candidates
}
fn push_parser_semantic_name_candidate(
candidates: &mut Vec<ParserSemanticNameCandidateV0>,
kind: NameKind,
text: &str,
) {
candidates.push(ParserSemanticNameCandidateV0 {
kind,
text: text.to_string(),
});
}
fn count_parser_semantic_name_kind(
candidates: &[ParserSemanticNameCandidateV0],
kind: NameKind,
) -> usize {
candidates
.iter()
.filter(|candidate| candidate.kind == kind)
.count()
}
fn intern_parser_semantic_name(db: &dyn salsa::Database, kind: NameKind, text: &str) -> bool {
match kind {
NameKind::ClassName => intern_class_name(db, text).is_ok(),
NameKind::CssIdent => intern_css_ident(db, text).is_ok(),
NameKind::PropertyName => intern_property_name(db, text).is_ok(),
NameKind::SelectorKey => intern_selector_key(db, text).is_ok(),
NameKind::CustomPropertyName => intern_custom_property_name(db, text).is_ok(),
NameKind::KeyframesName => intern_keyframes_name(db, text).is_ok(),
NameKind::MixinName => intern_mixin_name(db, text).is_ok(),
NameKind::FilePath => intern_file_path(db, text).is_ok(),
}
}
pub fn summarize_pratt_value_parser_coverage() -> ParserPrattValueCoverageSummaryV0 {
ParserPrattValueCoverageSummaryV0 {
product: "omena-parser.pratt-value-coverage",
infix_operator_kinds: vec![
SyntaxKind::Plus,
SyntaxKind::Minus,
SyntaxKind::Star,
SyntaxKind::Slash,
SyntaxKind::Percent,
],
prefix_operator_kinds: vec![SyntaxKind::Plus, SyntaxKind::Minus],
value_expression_node_kinds: vec![
SyntaxKind::UnaryExpression,
SyntaxKind::BinaryExpression,
SyntaxKind::ParenthesizedExpression,
SyntaxKind::FunctionCall,
SyntaxKind::FunctionArguments,
SyntaxKind::ValueList,
SyntaxKind::ComponentValueList,
SyntaxKind::SimpleBlock,
SyntaxKind::BogusValue,
],
specialized_function_family_count: 10,
css_values_l4_math_function_count: VALUES_L4_MATH_FUNCTION_NAMES.len(),
css_color_function_count: CSS_COLOR_FUNCTION_NAMES.len(),
ready_surfaces: vec![
"prattValueParserCore",
"prefixUnaryExpressions",
"additiveMultiplicativePrecedence",
"parenthesizedValueExpressions",
"functionArgumentValueLists",
"specializedCssValueFunctionFamilies",
"valuesL4MathFunctionArityChecks",
"varEnvAttrFunctionHeadChecks",
"dynamicInterpolationEscapeHatches",
"valueBogusRecovery",
],
next_surfaces: vec!["fullPropertyValueGrammarRegistry"],
}
}
pub fn summarize_recursive_descent_parser_coverage() -> ParserRecursiveDescentCoverageSummaryV0 {
ParserRecursiveDescentCoverageSummaryV0 {
product: "omena-parser.recursive-descent-coverage",
dialect_count: 4,
entry_point_count: 10,
selector_surface_count: 12,
at_rule_surface_count: 19,
dialect_extension_surface_count: 17,
recovery_surface_count: 8,
ready_surfaces: vec![
"recursiveDescentParserCore",
"stylesheetRuleDeclarationEntryPoints",
"selectorsLevelFourCstNodes",
"registeredAtRulePreludeParsers",
"cssNestingRuleItems",
"scssDialectStatements",
"sassIndentedBlocks",
"lessDialectStatements",
"bogusRecoverySkeleton",
"styleFactExtractionSurface",
],
next_surfaces: vec!["completeExternalSpecMirror"],
}
}
pub fn summarize_parser_boundary() -> ParserBoundarySummary {
ParserBoundarySummary {
product: "omena-parser.boundary",
tree_model: "cstree-green-root",
parser_track: "greenFieldNextToEngineStyleParser",
dialect_count: 4,
shared_name_kind_count: NameKind::ALL.len(),
ready_surfaces: vec![
"lexResult",
"lexedTokenTextSurface",
"parseResult",
"panicFreeTokenizer",
"cstreeGreenBuilder",
"tokenSetRecoveryScaffold",
"dialectExtensionScaffold",
"recursiveDescentParserCore",
"recursiveDescentCoverageSummary",
"selectorCstSkeleton",
"atRuleRegistrySkeleton",
"prattValueExpressionSkeleton",
"prattValueParserCore",
"prattValueCoverageSummary",
"attributeMatcherTokenization",
"attributeMatcherCstNodes",
"attributeNameValueModifierCstNodes",
"specializedValueFunctionCstNodes",
"caseInsensitiveFunctionRegistry",
"caseInsensitiveAtRuleRegistry",
"valueAtomCstNodes",
"identifierValueCstNodes",
"stringValueCstNodes",
"unicodeRangeValueCstNodes",
"functionArgumentValueLists",
"cssModuleScopeFunctionCstNodes",
"cssModuleGlobalSelectorFactFiltering",
"cssModuleLocalIdSelectorFacts",
"cssModuleValueStyleFacts",
"cssModuleValueDeclarationReferenceFacts",
"cssModuleComposesStyleFacts",
"icssStyleFacts",
"animationNameStyleFacts",
"animationShorthandStyleFacts",
"scssStructuredBlockAtRules",
"scssControlPreludeValidation",
"scssControlStyleFactExtraction",
"scssIncludeContentBlockStyleFacts",
"scssSassModuleEdgeStyleFacts",
"scssSassSymbolStyleFacts",
"scssUtilityAtRules",
"scssVariableFlagCstNodes",
"scssNestedPropertyCstNodes",
"scssModulePreludeSourceValidation",
"scssModulePreludeClauseValidation",
"scssModuleConfigCstNodes",
"scssModuleConfigBogusRecovery",
"scssPlaceholderSelectorCstNodes",
"lessMixinDeclarationCstNodes",
"lessMixinCallCstNodes",
"lessMixinGuardCstNodes",
"lessExtendPseudoCstNodes",
"lessDetachedRulesetCstNodes",
"lessNamespaceAccessCstNodes",
"lessPropertyVariableTokenization",
"lessPropertyVariableCstNodes",
"lessEscapedStringTokenization",
"lessEscapedStringValueCstNodes",
"importantAnnotationTokenization",
"urlTokenization",
"urlValueCstNodes",
"quotedUrlFunctionValueCstNodes",
"conditionalAtRulePreludeCstNodes",
"supportsAtRulePreludeValidation",
"conditionalLevel5AtRuleCstNodes",
"mediaQueryCstNodes",
"mediaQueryListValidation",
"importPreludeCstNodes",
"importSourcePreludeValidation",
"importTailPreludeValidation",
"customMediaPreludeValidation",
"propertyAtRuleNameValidation",
"namedAtRulePreludeValidation",
"containerAtRulePreludeValidation",
"charsetNamespaceAtRulePreludeValidation",
"keyframesAtRuleNameValidation",
"emptyBlockAtRulePreludeValidation",
"layerScopePreludeCstNodes",
"layerAtRulePreludeValidation",
"scopeAtRulePreludeValidation",
"pageAtRulePreludeValidation",
"pageMarginAtRuleCstNodes",
"modernDeclarationAtRuleCstNodes",
"fontFeatureValuesAtRuleCstNodes",
"fontFeatureValuesPreludeValidation",
"keyframeSelectorListValidation",
"viewTransitionAtRuleCstNodes",
"genericAtRulePreludeCstNodes",
"bogusAtRulePreludeCstNodes",
"nestingAtRuleCstNodes",
"customMediaAtRuleCstNodes",
"cssColorFunctionCstNodes",
"colorFunctionArgumentChecks",
"gradientFunctionCstNodes",
"transformFunctionCstNodes",
"filterFunctionCstNodes",
"imageFunctionCstNodes",
"shapeFunctionCstNodes",
"envAttrFunctionCstNodes",
"mathFunctionCstNodes",
"mathFunctionArityChecks",
"mathFunctionEmptyArgumentChecks",
"varEnvAttrFunctionHeadChecks",
"scssInterpolationTokenization",
"scssInterpolationCstNodes",
"lessInterpolationTokenization",
"lessInterpolationCstNodes",
"interpolationBogusRecovery",
"unicodeRangeTokenization",
"badStringTokenRecovery",
"badStringValueBogusNodes",
"emptyDeclarationValueRecovery",
"emptyVariableValueRecovery",
"missingSemicolonDeclarationRecovery",
"coreBogusPopulationSlice",
"dialectBogusPopulationSlice",
"cssModuleValueCstNodes",
"cssModuleComposesCstNodes",
"icssModuleBlockCstNodes",
"icssImportSourceValidation",
"cssModuleFromClauseSourceValidation",
"cssModuleComposesMultipleFromValidation",
"cssModuleGlobalComposesValidation",
"cssModuleBogusRecovery",
"valueListCstNodes",
"valueListBogusRecovery",
"genericRecoveryBogusNodes",
"sassIndentedTokenization",
"sassIndentedBlockCstNodes",
"sassIndentedStyleFacts",
"differentialCorpusSeed",
"differentialCorpus",
"lightningCssDifferentialCorpusSlice",
"lightningCssSelectorIdAndAtRuleDifferentialSlice",
"midTypingNoPanicPropertySlice",
"deterministicPanicFreeCorpus",
"losslessCstTextRoundTripSmoke",
"parseResultSourceTextSurface",
"parseSourceParseRoundTripSmoke",
"typedNumericValueAtomCstNodes",
"bracketedValueCstNodes",
"importantAnnotationCstNodes",
"splitImportantAnnotationCstNodes",
"unexpectedValueTokenBogusNodes",
"cdoCdcTokenization",
"cssIdentifierEscapeTokenization",
"nullAndBomInputPreprocessingSlice",
"hashDelimiterTokenization",
"cssDashIdentTokenization",
"signedNumericTokenization",
"exponentNumericTokenization",
"badUrlWhitespaceRecovery",
"parserEntryPointApiSlice",
"ruleListEntryPointApiSlice",
"componentValueEntryPointApiSlice",
"componentValueListEntryPointApiSlice",
"commaSeparatedComponentValueListEntryPointApiSlice",
"simpleBlockEntryPointApiSlice",
"typedCstWrapperSlice",
"parserCstEquivalence",
"typedBogusCstWrapperSlice",
"componentValueCstNodes",
"simpleBlockCstNodes",
"fullBogusPopulation",
"componentValueListCstNodes",
"commaSeparatedComponentValueListCstNodes",
"customPropertyAnyValueComponentList",
"customPropertyValueCstNodes",
"functionalPseudoSelectorListCstNodes",
"strictNotPseudoSelectorListCstNodes",
"nthSelectorOfSelectorListCstNodes",
"nthSelectorFormulaCstNodes",
"hasRelativeSelectorListCstNodes",
"langDirSelectorArgumentCstNodes",
"namespaceQualifiedSelectorCstNodes",
"selectorFunctionArgumentFactExclusion",
"missingBlockCloseBogusTrivia",
"initialDialectStatementNodes",
"recoveryBogusSkeleton",
"styleFactExtractionSurface",
"parserSemanticNameConsumption",
"productCutoverGate",
],
not_ready_surfaces: vec![
"completeExternalSpecMirror",
"fullPropertyValueGrammarRegistry",
],
}
}
fn tokenize<'text>(
text: &'text str,
extension: &impl DialectExtension,
) -> (Vec<Token<'text>>, Vec<ParseError>) {
let mut tokenizer = Tokenizer::new(text, extension);
tokenizer.tokenize();
(tokenizer.tokens, tokenizer.errors)
}
struct Tokenizer<'text, 'extension, E> {
text: &'text str,
extension: &'extension E,
offset: usize,
scss_interpolation_depth: usize,
less_interpolation_depth: usize,
sass_indent_stack: Vec<usize>,
tokens: Vec<Token<'text>>,
errors: Vec<ParseError>,
}
struct Parser<'text> {
tokens: Vec<Token<'text>>,
position: usize,
dialect: StyleDialect,
builder: GreenNodeBuilder<'static, 'static, SyntaxKind>,
errors: Vec<ParseError>,
}
impl<'text> Parser<'text> {
fn new(tokens: Vec<Token<'text>>, errors: Vec<ParseError>, dialect: StyleDialect) -> Self {
Self {
tokens,
position: 0,
dialect,
builder: GreenNodeBuilder::new(),
errors,
}
}
fn parse(&mut self) -> (GreenNode, Option<Arc<TokenInterner>>) {
self.parse_entry_point(ParseEntryPoint::Stylesheet)
}
fn parse_entry_point(
&mut self,
entry_point: ParseEntryPoint,
) -> (GreenNode, Option<Arc<TokenInterner>>) {
self.builder.start_node(SyntaxKind::Root);
match entry_point {
ParseEntryPoint::Stylesheet => {
self.builder.start_node(SyntaxKind::Stylesheet);
self.parse_stylesheet_items();
self.builder.finish_node();
}
ParseEntryPoint::RuleList => {
self.builder.start_node(SyntaxKind::RuleList);
self.parse_rule_list_items();
self.builder.finish_node();
}
ParseEntryPoint::Rule => self.parse_rule(),
ParseEntryPoint::DeclarationList => {
self.builder.start_node(SyntaxKind::DeclarationList);
self.parse_declaration_list();
self.builder.finish_node();
}
ParseEntryPoint::Declaration => self.parse_declaration(),
ParseEntryPoint::Value => {
self.builder.start_node(SyntaxKind::Value);
self.parse_value_or_value_list_until(&[]);
self.builder.finish_node();
}
ParseEntryPoint::ComponentValue => self.parse_component_value(&[]),
ParseEntryPoint::ComponentValueList => self.parse_component_value_list_until(&[]),
ParseEntryPoint::CommaSeparatedComponentValueList => {
self.parse_comma_separated_component_value_list_until(&[])
}
ParseEntryPoint::SimpleBlock => self.parse_simple_block_entry_point(&[]),
}
self.parse_sass_indentation_bogus();
self.parse_entry_point_trailing_bogus();
self.builder.finish_node();
let builder = std::mem::take(&mut self.builder);
let (green, cache) = builder.finish();
let interner = cache.and_then(|cache| cache.into_interner()).map(Arc::new);
(green, interner)
}
fn parse_sass_indentation_bogus(&mut self) {
if self.dialect != StyleDialect::Sass
|| !self
.errors
.iter()
.any(|error| error.message == "inconsistent Sass indentation")
{
return;
}
self.builder.start_node(SyntaxKind::BogusSassIndentation);
self.builder.finish_node();
}
fn parse_entry_point_trailing_bogus(&mut self) {
self.eat_trivia();
if self.at_end() {
return;
}
self.builder.start_node(SyntaxKind::BogusRecovery);
while !self.at_end() {
self.token_current();
}
self.builder.finish_node();
}
fn into_errors(self) -> Vec<ParseError> {
self.errors
}
fn parse_stylesheet_items(&mut self) {
while !self.at_end() {
self.eat_trivia();
if self.at_end() {
break;
}
match self.current_kind() {
Some(SyntaxKind::AtKeyword) if self.current_is_css_module_value_rule() => {
self.parse_css_module_value_rule()
}
Some(SyntaxKind::AtKeyword) if self.current_dialect_at_rule_spec().is_some() => {
self.parse_dialect_at_rule()
}
Some(SyntaxKind::AtKeyword) => self.parse_at_rule(),
Some(SyntaxKind::ScssVariable)
if matches!(self.dialect, StyleDialect::Scss | StyleDialect::Sass) =>
{
self.parse_variable_declaration(SyntaxKind::ScssVariableDeclaration)
}
Some(SyntaxKind::LessVariable) if self.dialect == StyleDialect::Less => {
self.parse_variable_declaration(SyntaxKind::LessVariableDeclaration)
}
Some(SyntaxKind::Cdo | SyntaxKind::Cdc) => self.token_current(),
Some(SyntaxKind::RightBrace | SyntaxKind::SassDedent) => self.token_current(),
Some(SyntaxKind::Semicolon | SyntaxKind::SassOptionalSemicolon) => {
self.token_current()
}
Some(_) => self.parse_rule(),
None => break,
}
}
}
fn parse_rule(&mut self) {
let starts_less_mixin =
self.dialect == StyleDialect::Less && self.current_starts_less_callable_signature();
let has_rule_block = self.find_rule_block_open_before_recovery(&[
SyntaxKind::Semicolon,
SyntaxKind::SassOptionalSemicolon,
SyntaxKind::RightBrace,
SyntaxKind::SassDedent,
]);
let kind = if let Some(kind) = self
.current_icss_module_rule_kind()
.filter(|_| has_rule_block)
{
kind
} else if self.current_starts_less_mixin_declaration() {
SyntaxKind::LessMixinDeclaration
} else if starts_less_mixin {
SyntaxKind::BogusLessMixin
} else if has_rule_block {
SyntaxKind::Rule
} else {
SyntaxKind::BogusRule
};
self.builder.start_node(kind);
if kind == SyntaxKind::CssModuleImportBlock && !self.current_icss_import_has_source() {
self.error_at_current(ParseErrorCode::ExpectedValue, "expected ICSS import source");
}
if kind == SyntaxKind::LessMixinDeclaration {
self.parse_less_mixin_header();
} else if kind == SyntaxKind::BogusLessMixin {
self.parse_until_recovery_with_optional_less_guard(&[
SyntaxKind::Semicolon,
SyntaxKind::RightBrace,
SyntaxKind::SassDedent,
]);
self.error_at_current(
ParseErrorCode::UnexpectedCharacter,
"expected Less mixin block",
);
} else {
self.parse_selector_list();
}
if self.current_kind() == Some(SyntaxKind::LeftBrace) {
self.token_current();
self.builder
.start_node(if self.previous_left_brace_has_match() {
SyntaxKind::DeclarationList
} else {
SyntaxKind::BogusDeclarationList
});
self.parse_declaration_list();
self.builder.finish_node();
if self.current_kind() == Some(SyntaxKind::RightBrace) {
self.token_current();
} else {
self.missing_token_bogus_trivia(
ParseErrorCode::UnexpectedCharacter,
"unterminated declaration block",
);
}
} else if self.current_kind() == Some(SyntaxKind::SassIndent) {
self.builder.start_node(SyntaxKind::SassIndentedBlock);
self.token_current();
self.builder.start_node(SyntaxKind::DeclarationList);
self.parse_declaration_list();
self.builder.finish_node();
if self.current_kind() == Some(SyntaxKind::SassDedent) {
self.token_current();
} else {
self.missing_token_bogus_trivia(
ParseErrorCode::UnexpectedCharacter,
"unterminated Sass indented declaration block",
);
}
self.builder.finish_node();
} else {
self.consume_until_recovery(&[
SyntaxKind::Semicolon,
SyntaxKind::SassOptionalSemicolon,
SyntaxKind::RightBrace,
SyntaxKind::SassDedent,
]);
if self.current_kind().is_some_and(is_statement_end) {
self.token_current();
}
}
self.builder.finish_node();
}
fn current_icss_module_rule_kind(&self) -> Option<SyntaxKind> {
if self.current_kind() != Some(SyntaxKind::Colon) {
return None;
}
let (name_index, name_kind) = self.non_trivia_token_from(self.position + 1)?;
if name_kind != SyntaxKind::Ident {
return None;
}
match self.tokens.get(name_index)?.text {
"export" => Some(SyntaxKind::CssModuleExportBlock),
"import" => Some(SyntaxKind::CssModuleImportBlock),
_ => None,
}
}
fn current_icss_import_has_source(&self) -> bool {
let Some((name_index, SyntaxKind::Ident)) = self.non_trivia_token_from(self.position + 1)
else {
return false;
};
if self
.tokens
.get(name_index)
.is_none_or(|token| token.text != "import")
{
return false;
}
let Some((open_index, SyntaxKind::LeftParen)) = self.non_trivia_token_from(name_index + 1)
else {
return false;
};
let Some((_, source_kind)) = self.non_trivia_token_from(open_index + 1) else {
return false;
};
matches!(
source_kind,
SyntaxKind::String | SyntaxKind::Url | SyntaxKind::ScssInterpolationStart
)
}
fn parse_selector_list(&mut self) {
self.parse_selector_list_until(&[]);
}
fn parse_selector_list_until(&mut self, recovery: &[SyntaxKind]) {
let kind = if self.current_kind() == Some(SyntaxKind::LeftBrace) {
SyntaxKind::BogusSelectorList
} else {
SyntaxKind::SelectorList
};
self.builder.start_node(kind);
while !self.at_end() {
match self.current_kind() {
Some(SyntaxKind::Comma) => self.token_current(),
Some(kind) if is_selector_boundary_until(kind, recovery) => break,
Some(SyntaxKind::SassIndentedNewline) => self.token_current(),
Some(_)
if recovery.contains(&SyntaxKind::RightParen)
&& self.current_selector_item_is_bogus(recovery) =>
{
self.parse_bogus_selector_until(recovery)
}
Some(_) => self.parse_selector_until(recovery),
None => break,
}
}
self.builder.finish_node();
}
fn parse_strict_selector_list_until(&mut self, recovery: &[SyntaxKind]) {
self.builder.start_node(
if self.selector_list_contains_bogus_item_until(recovery)
&& self.current_kind() != Some(SyntaxKind::RightParen)
{
SyntaxKind::BogusSelectorList
} else {
SyntaxKind::SelectorList
},
);
while !self.at_end() {
match self.current_kind() {
Some(SyntaxKind::Comma) => self.token_current(),
Some(kind) if is_selector_boundary_until(kind, recovery) => break,
Some(SyntaxKind::SassIndentedNewline) => self.token_current(),
Some(_)
if self.current_selector_item_is_bogus(recovery)
&& self.current_kind() != Some(SyntaxKind::RightParen) =>
{
self.parse_bogus_selector_until(recovery)
}
Some(_) => self.parse_selector_until(recovery),
None => break,
}
}
self.builder.finish_node();
}
fn parse_relative_selector_list_until(&mut self, recovery: &[SyntaxKind]) {
self.builder.start_node(
if self.current_selector_item_is_bogus(recovery)
&& self.current_kind() != Some(SyntaxKind::RightParen)
{
SyntaxKind::BogusSelectorList
} else {
SyntaxKind::RelativeSelectorList
},
);
while !self.at_end() {
match self.current_kind() {
Some(SyntaxKind::Comma) => self.token_current(),
Some(kind) if is_selector_boundary_until(kind, recovery) => break,
Some(SyntaxKind::SassIndentedNewline) => self.token_current(),
Some(_)
if self.current_selector_item_is_bogus(recovery)
&& self.current_kind() != Some(SyntaxKind::RightParen) =>
{
self.parse_bogus_selector_until(recovery)
}
Some(_) => self.parse_relative_selector_until(recovery),
None => break,
}
}
self.builder.finish_node();
}
fn parse_relative_selector_until(&mut self, recovery: &[SyntaxKind]) {
self.builder.start_node(SyntaxKind::RelativeSelector);
self.builder.start_node(SyntaxKind::ComplexSelector);
self.parse_complex_selector_until(recovery);
self.builder.finish_node();
self.builder.finish_node();
}
fn parse_bogus_selector_until(&mut self, recovery: &[SyntaxKind]) {
self.builder.start_node(SyntaxKind::BogusSelector);
self.error_at_current(
ParseErrorCode::UnexpectedCharacter,
"invalid selector in selector list",
);
let mut paren_depth = 0usize;
let mut bracket_depth = 0usize;
while !self.at_end() {
let Some(kind) = self.current_kind() else {
break;
};
if paren_depth == 0
&& bracket_depth == 0
&& (kind == SyntaxKind::Comma || is_selector_boundary_until(kind, recovery))
{
break;
}
match kind {
SyntaxKind::LeftParen => paren_depth += 1,
SyntaxKind::RightParen => paren_depth = paren_depth.saturating_sub(1),
SyntaxKind::LeftBracket => bracket_depth += 1,
SyntaxKind::RightBracket => bracket_depth = bracket_depth.saturating_sub(1),
_ => {}
}
self.token_current();
}
self.builder.finish_node();
}
fn parse_selector_until(&mut self, recovery: &[SyntaxKind]) {
self.builder.start_node(SyntaxKind::Selector);
self.builder.start_node(SyntaxKind::ComplexSelector);
self.parse_complex_selector_until(recovery);
self.builder.finish_node();
self.builder.finish_node();
}
fn parse_complex_selector_until(&mut self, recovery: &[SyntaxKind]) {
let mut has_component = false;
while !self.at_end() {
match self.current_kind() {
Some(kind) if is_selector_boundary_until(kind, recovery) => break,
Some(SyntaxKind::Whitespace) => {
if has_component
&& self.next_non_trivia_kind().is_some_and(|kind| {
!is_selector_boundary_until(kind, recovery) && !is_combinator(kind)
})
{
self.parse_whitespace_combinator();
has_component = false;
} else {
self.token_current();
}
}
Some(SyntaxKind::SassIndentedNewline) => self.token_current(),
Some(kind) if is_combinator(kind) => {
self.parse_combinator();
has_component = false;
}
Some(_) => {
self.parse_compound_selector_until(recovery);
has_component = true;
}
None => break,
}
}
}
fn parse_compound_selector_until(&mut self, recovery: &[SyntaxKind]) {
let starts_valid = self.current_kind().is_some_and(|kind| {
selector_component_can_start(kind)
|| self.current_starts_namespace_qualified_selector(kind)
|| is_interpolation_start(kind)
});
self.builder.start_node(if starts_valid {
SyntaxKind::CompoundSelector
} else {
SyntaxKind::BogusCompoundSelector
});
let start = self.position;
while !self.at_end() {
match self.current_kind() {
Some(kind)
if is_selector_boundary_until(kind, recovery)
|| kind == SyntaxKind::Whitespace
|| kind == SyntaxKind::SassIndentedNewline
|| is_combinator(kind) =>
{
break;
}
Some(SyntaxKind::Dot) => self.parse_class_selector(),
Some(SyntaxKind::Hash) => self.parse_id_selector(),
Some(kind) if self.current_starts_namespace_qualified_selector(kind) => {
self.parse_namespace_qualified_selector()
}
Some(SyntaxKind::Ident) => self.parse_type_selector(),
Some(SyntaxKind::Star) => self.parse_universal_selector(),
Some(SyntaxKind::Ampersand) => self.parse_nesting_selector(),
Some(SyntaxKind::ScssPlaceholder) => self.parse_scss_placeholder_selector(),
Some(kind) if is_interpolation_start(kind) => self.parse_interpolation(
kind,
&[
SyntaxKind::Comma,
SyntaxKind::LeftBrace,
SyntaxKind::SassIndent,
SyntaxKind::RightBrace,
SyntaxKind::SassDedent,
SyntaxKind::RightParen,
SyntaxKind::Semicolon,
SyntaxKind::SassOptionalSemicolon,
],
),
Some(SyntaxKind::LeftBracket) => self.parse_attribute_selector(),
Some(SyntaxKind::Colon) if self.current_starts_less_extend_rule() => {
self.parse_less_extend_rule()
}
Some(SyntaxKind::Colon) => {
self.parse_pseudo_selector(SyntaxKind::PseudoClassSelector)
}
Some(SyntaxKind::DoubleColon) => {
self.parse_pseudo_selector(SyntaxKind::PseudoElementSelector)
}
Some(_) => self.token_current(),
None => break,
}
}
if self.position == start {
self.token_current();
}
if !starts_valid {
self.error_at_current(
ParseErrorCode::UnexpectedCharacter,
"expected selector component",
);
}
self.builder.finish_node();
}
fn parse_class_selector(&mut self) {
self.builder.start_node(SyntaxKind::ClassSelector);
self.token_current();
if matches!(
self.current_kind(),
Some(SyntaxKind::Ident | SyntaxKind::CustomPropertyName)
) {
self.token_current();
} else {
self.empty_bogus_node(
SyntaxKind::BogusSelector,
ParseErrorCode::ExpectedSelectorName,
"expected class selector name",
);
}
self.builder.finish_node();
}
fn parse_id_selector(&mut self) {
self.builder.start_node(SyntaxKind::IdSelector);
self.token_current();
self.builder.finish_node();
}
fn parse_type_selector(&mut self) {
self.builder.start_node(SyntaxKind::TypeSelector);
self.token_current();
self.builder.finish_node();
}
fn parse_universal_selector(&mut self) {
self.builder.start_node(SyntaxKind::UniversalSelector);
self.token_current();
self.builder.finish_node();
}
fn parse_namespace_qualified_selector(&mut self) {
let selector_kind =
if self.namespace_qualified_selector_target_kind() == Some(SyntaxKind::Star) {
SyntaxKind::UniversalSelector
} else {
SyntaxKind::TypeSelector
};
self.builder.start_node(selector_kind);
self.builder.start_node(SyntaxKind::NamespacePrefix);
if self.current_kind() != Some(SyntaxKind::Pipe) {
self.token_current();
}
self.token_current();
self.builder.finish_node();
if matches!(
self.current_kind(),
Some(SyntaxKind::Ident | SyntaxKind::CustomPropertyName | SyntaxKind::Star)
) {
self.token_current();
} else {
self.empty_bogus_node(
SyntaxKind::BogusSelector,
ParseErrorCode::ExpectedSelectorName,
"expected namespace-qualified selector name",
);
}
self.builder.finish_node();
}
fn parse_nesting_selector(&mut self) {
self.builder.start_node(SyntaxKind::NestingSelectorNode);
self.token_current();
self.builder.finish_node();
}
fn parse_scss_placeholder_selector(&mut self) {
self.builder.start_node(SyntaxKind::ScssPlaceholderSelector);
self.token_current();
self.builder.finish_node();
}
fn parse_attribute_selector(&mut self) {
let kind = if self.find_before_recovery(
SyntaxKind::RightBracket,
&[
SyntaxKind::Comma,
SyntaxKind::LeftBrace,
SyntaxKind::RightBrace,
SyntaxKind::Semicolon,
],
) {
SyntaxKind::AttributeSelector
} else {
SyntaxKind::BogusSelector
};
self.builder.start_node(kind);
self.token_current();
let mut saw_matcher = false;
let mut saw_value = false;
let mut closed = false;
while !self.at_end() {
match self.current_kind() {
Some(SyntaxKind::RightBracket) => {
self.token_current();
closed = true;
break;
}
Some(kind) if is_attribute_matcher(kind) => {
self.parse_attribute_matcher();
saw_matcher = true;
}
Some(kind) if is_selector_boundary(kind) => break,
Some(kind) if !saw_matcher && attribute_name_token_can_start(kind) => {
self.parse_attribute_name()
}
Some(kind)
if saw_matcher && !saw_value && attribute_value_token_can_start(kind) =>
{
self.parse_attribute_value();
saw_value = true;
}
Some(SyntaxKind::Ident | SyntaxKind::CustomPropertyName) if saw_value => {
self.parse_attribute_modifier()
}
Some(_) => self.token_current(),
None => break,
}
}
if !closed {
self.error_at_current(
ParseErrorCode::UnterminatedAttributeSelector,
"unterminated attribute selector",
);
}
self.builder.finish_node();
}
fn parse_attribute_matcher(&mut self) {
self.builder.start_node(SyntaxKind::AttributeMatcher);
self.token_current();
self.builder.finish_node();
}
fn parse_attribute_name(&mut self) {
self.builder.start_node(SyntaxKind::AttributeName);
while !self.at_end() {
match self.current_kind() {
Some(SyntaxKind::RightBracket) => break,
Some(kind) if is_attribute_matcher(kind) || is_selector_boundary(kind) => break,
Some(kind) if attribute_name_token_can_continue(kind) => self.token_current(),
Some(_) => break,
None => break,
}
}
self.builder.finish_node();
}
fn parse_attribute_value(&mut self) {
self.builder.start_node(SyntaxKind::AttributeValue);
self.token_current();
self.builder.finish_node();
}
fn parse_attribute_modifier(&mut self) {
self.builder.start_node(SyntaxKind::AttributeModifier);
self.token_current();
self.builder.finish_node();
}
fn parse_pseudo_selector(&mut self, kind: SyntaxKind) {
self.builder.start_node(kind);
self.token_current();
let pseudo_name = self.current_text().map(str::to_owned);
let css_module_scope_kind = if kind == SyntaxKind::PseudoClassSelector {
self.current_text().and_then(css_module_scope_function_kind)
} else {
None
};
if self.current_kind() == Some(SyntaxKind::Ident) {
if let Some(kind) = css_module_scope_kind {
self.builder.start_node(kind);
}
self.token_current();
} else {
self.empty_bogus_node(
SyntaxKind::BogusSelector,
ParseErrorCode::ExpectedSelectorName,
"expected pseudo selector name",
);
}
if self.current_kind() == Some(SyntaxKind::LeftParen) {
self.token_current();
self.builder.start_node(SyntaxKind::PseudoSelectorArgument);
if kind == SyntaxKind::PseudoClassSelector
&& pseudo_name
.as_deref()
.is_some_and(is_selector_list_pseudo_class)
{
self.parse_selector_list_until(&[SyntaxKind::RightParen]);
} else if kind == SyntaxKind::PseudoClassSelector
&& pseudo_name.as_deref() == Some("not")
{
self.parse_strict_selector_list_until(&[SyntaxKind::RightParen]);
} else if kind == SyntaxKind::PseudoClassSelector
&& pseudo_name.as_deref() == Some("has")
{
self.parse_relative_selector_list_until(&[SyntaxKind::RightParen]);
} else if kind == SyntaxKind::PseudoClassSelector
&& pseudo_name.as_deref().is_some_and(is_nth_pseudo_class)
{
self.parse_nth_selector_argument();
} else if kind == SyntaxKind::PseudoClassSelector
&& pseudo_name.as_deref() == Some("lang")
{
self.parse_language_selector_argument();
} else if kind == SyntaxKind::PseudoClassSelector
&& pseudo_name.as_deref() == Some("dir")
{
self.parse_directionality_selector_argument();
} else {
while !self.at_end() {
match self.current_kind() {
Some(SyntaxKind::RightParen) => break,
Some(kind) if is_selector_boundary(kind) => break,
Some(_) => self.token_current(),
None => break,
}
}
}
self.builder.finish_node();
if self.current_kind() == Some(SyntaxKind::RightParen) {
self.token_current();
}
}
if css_module_scope_kind.is_some() {
self.builder.finish_node();
}
self.builder.finish_node();
}
fn parse_nth_selector_argument(&mut self) {
self.builder.start_node(SyntaxKind::NthSelectorArgument);
self.builder.start_node(SyntaxKind::NthSelectorFormula);
while !self.at_end() {
match self.current_kind() {
Some(SyntaxKind::RightParen) => break,
Some(kind) if is_selector_boundary(kind) => break,
Some(SyntaxKind::Ident) if self.current_text() == Some("of") => break,
Some(_) => self.token_current(),
None => break,
}
}
self.builder.finish_node();
if self.current_kind() == Some(SyntaxKind::Ident) && self.current_text() == Some("of") {
self.builder
.start_node(SyntaxKind::NthSelectorOfSelectorList);
self.token_current();
self.parse_selector_list_until(&[SyntaxKind::RightParen]);
self.builder.finish_node();
}
self.builder.finish_node();
}
fn parse_language_selector_argument(&mut self) {
self.builder
.start_node(SyntaxKind::LanguageSelectorArgument);
while !self.at_end() {
match self.current_kind() {
Some(SyntaxKind::RightParen) => break,
Some(SyntaxKind::Comma) => self.token_current(),
Some(kind) if is_selector_boundary(kind) => break,
Some(kind) if language_tag_token_can_start(kind) => self.parse_language_tag(),
Some(_) => self.token_current(),
None => break,
}
}
self.builder.finish_node();
}
fn parse_language_tag(&mut self) {
self.builder.start_node(SyntaxKind::LanguageTag);
self.token_current();
self.builder.finish_node();
}
fn parse_directionality_selector_argument(&mut self) {
self.builder
.start_node(SyntaxKind::DirectionalitySelectorArgument);
if self
.current_kind()
.is_some_and(language_tag_token_can_start)
{
self.token_current();
}
while !self.at_end() {
match self.current_kind() {
Some(SyntaxKind::RightParen) => break,
Some(kind) if is_selector_boundary(kind) => break,
Some(_) => self.token_current(),
None => break,
}
}
self.builder.finish_node();
}
fn parse_less_extend_rule(&mut self) {
self.builder.start_node(SyntaxKind::LessExtendRule);
if self.current_kind() == Some(SyntaxKind::Colon) {
self.token_current();
}
if self.current_text() == Some("extend") {
self.token_current();
} else {
self.empty_bogus_node(
SyntaxKind::BogusSelector,
ParseErrorCode::ExpectedSelectorName,
"expected Less extend selector",
);
}
if self.current_kind() == Some(SyntaxKind::LeftParen) {
self.token_current();
self.builder.start_node(SyntaxKind::PseudoSelectorArgument);
while !self.at_end() {
match self.current_kind() {
Some(SyntaxKind::RightParen) => break,
Some(kind) if is_selector_boundary(kind) => break,
Some(kind) if is_interpolation_start(kind) => self.parse_interpolation(
kind,
&[
SyntaxKind::RightParen,
SyntaxKind::Comma,
SyntaxKind::LeftBrace,
SyntaxKind::SassIndent,
SyntaxKind::Semicolon,
SyntaxKind::SassOptionalSemicolon,
],
),
Some(_) => self.token_current(),
None => break,
}
}
self.builder.finish_node();
if self.current_kind() == Some(SyntaxKind::RightParen) {
self.token_current();
}
}
self.builder.finish_node();
}
fn parse_combinator(&mut self) {
let has_rhs = self
.next_non_trivia_kind()
.is_some_and(|kind| selector_component_can_start(kind) || is_interpolation_start(kind));
self.builder.start_node(if has_rhs {
SyntaxKind::Combinator
} else {
SyntaxKind::BogusCombinator
});
self.token_current();
if !has_rhs {
self.error_at_current(
ParseErrorCode::UnexpectedCharacter,
"expected selector after combinator",
);
}
self.builder.finish_node();
}
fn parse_whitespace_combinator(&mut self) {
self.builder.start_node(SyntaxKind::Combinator);
while self.current_kind() == Some(SyntaxKind::Whitespace) {
self.token_current();
}
self.builder.finish_node();
}
fn parse_declaration_list(&mut self) {
while !self.at_end() {
self.eat_trivia();
match self.current_kind() {
Some(SyntaxKind::RightBrace | SyntaxKind::SassDedent) | None => break,
Some(SyntaxKind::Semicolon | SyntaxKind::SassOptionalSemicolon) => {
self.token_current()
}
Some(SyntaxKind::AtKeyword) if self.current_is_css_module_value_rule() => {
self.parse_css_module_value_rule()
}
Some(SyntaxKind::AtKeyword) if self.current_dialect_at_rule_spec().is_some() => {
self.parse_dialect_at_rule()
}
Some(SyntaxKind::AtKeyword) => self.parse_at_rule(),
Some(_) if self.current_starts_less_namespace_access() => {
self.parse_less_namespace_access()
}
Some(_) if self.current_starts_less_mixin_call() => self.parse_less_mixin_call(),
Some(_) if self.current_starts_scss_nested_property() => {
self.parse_scss_nested_property()
}
Some(_) if self.current_starts_nested_rule() => self.parse_rule(),
Some(SyntaxKind::ScssVariable)
if matches!(self.dialect, StyleDialect::Scss | StyleDialect::Sass) =>
{
self.parse_variable_declaration(SyntaxKind::ScssVariableDeclaration)
}
Some(SyntaxKind::LessVariable) if self.dialect == StyleDialect::Less => {
self.parse_variable_declaration(SyntaxKind::LessVariableDeclaration)
}
Some(SyntaxKind::LeftBrace) => {
self.builder.start_node(SyntaxKind::BogusDeclaration);
self.token_current();
self.builder.finish_node();
}
Some(_) => self.parse_declaration(),
}
}
}
fn parse_scss_nested_property(&mut self) {
self.builder.start_node(SyntaxKind::ScssNestedProperty);
self.builder.start_node(SyntaxKind::PropertyName);
while !self.at_end() {
match self.current_kind() {
Some(SyntaxKind::Colon) => break,
Some(
SyntaxKind::Semicolon
| SyntaxKind::SassOptionalSemicolon
| SyntaxKind::RightBrace
| SyntaxKind::SassDedent,
) => break,
Some(kind) if is_interpolation_start(kind) => self.parse_interpolation(
kind,
&[
SyntaxKind::Colon,
SyntaxKind::Semicolon,
SyntaxKind::SassOptionalSemicolon,
SyntaxKind::RightBrace,
SyntaxKind::SassDedent,
],
),
Some(_) => self.token_current(),
None => break,
}
}
self.builder.finish_node();
if self.current_kind() == Some(SyntaxKind::Colon) {
self.token_current();
}
let block_recovery = [
SyntaxKind::LeftBrace,
SyntaxKind::SassIndent,
SyntaxKind::Semicolon,
SyntaxKind::SassOptionalSemicolon,
SyntaxKind::RightBrace,
SyntaxKind::SassDedent,
];
if !matches!(
self.current_kind(),
Some(
SyntaxKind::LeftBrace
| SyntaxKind::SassIndent
| SyntaxKind::Semicolon
| SyntaxKind::SassOptionalSemicolon
| SyntaxKind::RightBrace
| SyntaxKind::SassDedent
)
) {
self.builder.start_node(SyntaxKind::Value);
self.parse_value_or_value_list_until(&block_recovery);
self.builder.finish_node();
}
match self.current_kind() {
Some(SyntaxKind::LeftBrace) => self.parse_declaration_block(),
Some(SyntaxKind::SassIndent) => self.parse_sass_indented_nested_property_block(),
Some(_) => self.consume_until_recovery(&[
SyntaxKind::Semicolon,
SyntaxKind::SassOptionalSemicolon,
SyntaxKind::RightBrace,
SyntaxKind::SassDedent,
]),
None => {}
}
if self.current_kind().is_some_and(is_statement_end) {
self.token_current();
}
self.builder.finish_node();
}
fn parse_sass_indented_nested_property_block(&mut self) {
self.builder.start_node(SyntaxKind::SassIndentedBlock);
if self.current_kind() == Some(SyntaxKind::SassIndent) {
self.token_current();
}
self.builder.start_node(SyntaxKind::DeclarationList);
self.parse_declaration_list();
self.builder.finish_node();
if self.current_kind() == Some(SyntaxKind::SassDedent) {
self.token_current();
} else {
self.error_at_current(
ParseErrorCode::UnexpectedCharacter,
"unterminated Sass indented nested property block",
);
}
self.builder.finish_node();
}
fn parse_variable_declaration(&mut self, kind: SyntaxKind) {
let has_colon = self.find_before_recovery(
SyntaxKind::Colon,
&[
SyntaxKind::Semicolon,
SyntaxKind::SassOptionalSemicolon,
SyntaxKind::RightBrace,
SyntaxKind::SassDedent,
],
);
self.builder
.start_node(variable_declaration_node_kind(kind, has_colon));
self.token_current();
if self.current_kind() == Some(SyntaxKind::Colon) {
self.token_current();
self.eat_value_trivia();
let value_recovery = [
SyntaxKind::Semicolon,
SyntaxKind::SassOptionalSemicolon,
SyntaxKind::RightBrace,
SyntaxKind::SassDedent,
];
if kind == SyntaxKind::LessVariableDeclaration
&& self.current_kind() == Some(SyntaxKind::LeftBrace)
{
self.parse_less_detached_ruleset();
} else {
let has_value = self
.non_trivia_token_from(self.position)
.is_some_and(|(_, kind)| !value_recovery.contains(&kind));
self.builder.start_node(SyntaxKind::Value);
if has_value {
self.parse_value_or_value_list_until(&value_recovery);
} else {
self.empty_bogus_node(
SyntaxKind::BogusValue,
ParseErrorCode::ExpectedValue,
"expected variable value",
);
}
self.builder.finish_node();
}
} else {
self.error_at_current(
ParseErrorCode::UnexpectedCharacter,
"expected variable declaration colon",
);
self.consume_until_recovery(&[
SyntaxKind::Semicolon,
SyntaxKind::SassOptionalSemicolon,
SyntaxKind::RightBrace,
SyntaxKind::SassDedent,
]);
}
if self.current_kind().is_some_and(is_statement_end) {
self.token_current();
}
self.builder.finish_node();
}
fn parse_less_detached_ruleset(&mut self) {
let closed = self.current_left_brace_has_match();
self.builder.start_node(if closed {
SyntaxKind::LessDetachedRulesetNode
} else {
SyntaxKind::BogusLessDetachedRuleset
});
if self.current_kind() == Some(SyntaxKind::LeftBrace) {
self.token_current();
self.builder.start_node(SyntaxKind::DeclarationList);
self.parse_declaration_list();
self.builder.finish_node();
}
if self.current_kind() == Some(SyntaxKind::RightBrace) {
self.token_current();
} else {
self.error_at_current(
ParseErrorCode::UnexpectedCharacter,
"unterminated Less detached ruleset",
);
}
self.builder.finish_node();
}
fn parse_declaration(&mut self) {
let starts_composes = self.current_text() == Some("composes");
let starts_custom_property = self.current_kind() == Some(SyntaxKind::CustomPropertyName);
let has_colon = self.find_before_recovery(
SyntaxKind::Colon,
&[
SyntaxKind::Semicolon,
SyntaxKind::SassOptionalSemicolon,
SyntaxKind::RightBrace,
SyntaxKind::SassDedent,
SyntaxKind::LeftBrace,
SyntaxKind::SassIndent,
],
);
let kind = if starts_composes && has_colon {
SyntaxKind::CssModuleComposesDeclaration
} else if starts_composes {
SyntaxKind::BogusComposesDeclaration
} else if has_colon {
SyntaxKind::Declaration
} else {
SyntaxKind::BogusDeclaration
};
self.builder.start_node(kind);
if kind == SyntaxKind::CssModuleComposesDeclaration
&& self.current_css_module_scope_context() == Some("global")
{
self.error_at_current(
ParseErrorCode::UnexpectedCharacter,
"composes is not allowed inside :global scope",
);
}
let property_kind = if matches!(
self.current_kind(),
Some(
SyntaxKind::Colon
| SyntaxKind::Semicolon
| SyntaxKind::SassOptionalSemicolon
| SyntaxKind::LeftBrace
| SyntaxKind::SassIndent
| SyntaxKind::RightBrace
| SyntaxKind::SassDedent
)
) {
SyntaxKind::BogusPropertyName
} else {
SyntaxKind::PropertyName
};
self.builder.start_node(property_kind);
while !self.at_end() {
match self.current_kind() {
Some(
SyntaxKind::Colon
| SyntaxKind::Semicolon
| SyntaxKind::SassOptionalSemicolon
| SyntaxKind::RightBrace
| SyntaxKind::SassDedent,
) => break,
Some(kind) if is_interpolation_start(kind) => self.parse_interpolation(
kind,
&[
SyntaxKind::Colon,
SyntaxKind::Semicolon,
SyntaxKind::SassOptionalSemicolon,
SyntaxKind::RightBrace,
SyntaxKind::SassDedent,
],
),
Some(_) => self.token_current(),
None => break,
}
}
self.builder.finish_node();
if property_kind == SyntaxKind::BogusPropertyName {
self.error_at_current(
ParseErrorCode::UnexpectedCharacter,
"expected declaration property name",
);
}
if self.current_kind() == Some(SyntaxKind::Colon) {
self.token_current();
let value_recovery = [
SyntaxKind::Semicolon,
SyntaxKind::SassOptionalSemicolon,
SyntaxKind::RightBrace,
SyntaxKind::SassDedent,
];
let has_value = self
.non_trivia_token_from(self.position)
.is_some_and(|(_, kind)| !value_recovery.contains(&kind));
self.builder.start_node(SyntaxKind::Value);
if kind == SyntaxKind::CssModuleComposesDeclaration {
self.parse_composes_value_until(&value_recovery);
} else if starts_custom_property {
self.builder.start_node(SyntaxKind::CustomPropertyValue);
self.parse_component_value_list_until(&value_recovery);
self.builder.finish_node();
} else if !has_value {
self.empty_bogus_node(
SyntaxKind::BogusValue,
ParseErrorCode::ExpectedValue,
"expected declaration value",
);
} else {
self.parse_declaration_value_or_value_list_until(&value_recovery);
}
self.builder.finish_node();
} else {
self.consume_until_recovery(&[
SyntaxKind::Semicolon,
SyntaxKind::SassOptionalSemicolon,
SyntaxKind::RightBrace,
SyntaxKind::SassDedent,
]);
}
if self.current_kind().is_some_and(is_statement_end) {
self.token_current();
}
self.builder.finish_node();
}
fn parse_composes_value_until(&mut self, recovery: &[SyntaxKind]) {
let mut saw_target = false;
if self.current_composes_value_has_multiple_from_clauses(recovery) {
self.error_at_current(
ParseErrorCode::UnexpectedCharacter,
"multiple composes from clauses are not allowed",
);
}
while !self.at_end() {
self.eat_value_trivia();
match self.current_kind() {
Some(kind) if recovery.contains(&kind) => break,
Some(SyntaxKind::Ident) if self.current_text() == Some("from") => {
if !saw_target {
self.empty_bogus_node(
SyntaxKind::BogusComposesTarget,
ParseErrorCode::UnexpectedCharacter,
"expected composes target before from clause",
);
saw_target = true;
}
self.parse_css_module_from_clause(recovery);
}
Some(SyntaxKind::Ident | SyntaxKind::CustomPropertyName) => {
self.builder.start_node(SyntaxKind::CssModuleComposesTarget);
self.token_current();
self.builder.finish_node();
saw_target = true;
}
Some(kind) if is_interpolation_start(kind) => {
self.parse_interpolation(kind, recovery)
}
Some(_) => self.token_current(),
None => break,
}
}
if !saw_target {
self.empty_bogus_node(
SyntaxKind::BogusComposesTarget,
ParseErrorCode::UnexpectedCharacter,
"expected composes target",
);
}
}
fn current_composes_value_has_multiple_from_clauses(&self, recovery: &[SyntaxKind]) -> bool {
let mut index = self.position;
let mut paren_depth = 0usize;
let mut bracket_depth = 0usize;
let mut brace_depth = 0usize;
let mut from_count = 0usize;
while let Some(token) = self.tokens.get(index) {
if paren_depth == 0
&& bracket_depth == 0
&& brace_depth == 0
&& recovery.contains(&token.kind)
{
break;
}
match token.kind {
SyntaxKind::LeftParen => paren_depth += 1,
SyntaxKind::RightParen => paren_depth = paren_depth.saturating_sub(1),
SyntaxKind::LeftBracket => bracket_depth += 1,
SyntaxKind::RightBracket => bracket_depth = bracket_depth.saturating_sub(1),
SyntaxKind::LeftBrace => brace_depth += 1,
SyntaxKind::RightBrace => brace_depth = brace_depth.saturating_sub(1),
SyntaxKind::Ident
if paren_depth == 0
&& bracket_depth == 0
&& brace_depth == 0
&& token.text == "from" =>
{
from_count += 1;
if from_count > 1 {
return true;
}
}
_ => {}
}
index += 1;
}
false
}
fn parse_css_module_from_clause(&mut self, recovery: &[SyntaxKind]) {
let source = self.non_trivia_token_from(self.position + 1);
let has_source = source.is_some_and(|(_, kind)| !recovery.contains(&kind));
let has_valid_source = source.is_some_and(|(index, kind)| {
self.tokens
.get(index)
.is_some_and(|token| is_css_module_from_source_token(kind, token.text))
});
self.builder.start_node(if has_valid_source {
SyntaxKind::CssModuleFromClause
} else {
SyntaxKind::BogusFromClause
});
self.token_current();
while !self.at_end() {
match self.current_kind() {
Some(kind) if recovery.contains(&kind) => break,
Some(_) => self.token_current(),
None => break,
}
}
if !has_source {
self.error_at_current(
ParseErrorCode::UnexpectedCharacter,
"expected CSS Modules from-clause source",
);
} else if !has_valid_source {
self.error_at_current(
ParseErrorCode::ExpectedValue,
"invalid CSS Modules from-clause source",
);
}
self.builder.finish_node();
}
fn current_css_module_scope_context(&self) -> Option<&'static str> {
let mut open_blocks = Vec::new();
for (index, token) in self.tokens.iter().take(self.position).enumerate() {
match token.kind {
SyntaxKind::LeftBrace | SyntaxKind::SassIndent => open_blocks.push(index),
SyntaxKind::RightBrace | SyntaxKind::SassDedent => {
open_blocks.pop();
}
_ => {}
}
}
if let Some(scope) = open_blocks.iter().copied().find_map(|block_start| {
let header_start = self.header_start_for_block(block_start);
css_module_block_scope_marker_in_header(&self.tokens, header_start, block_start)
}) {
return Some(scope);
}
let block_start = open_blocks.last().copied()?;
let header_start = self.header_start_for_block(block_start);
css_module_header_is_global_only(&self.tokens, header_start, block_start)
.then_some("global")
}
fn header_start_for_block(&self, block_start: usize) -> usize {
let mut index = block_start;
while index > 0 {
let previous = index - 1;
if matches!(
self.tokens[previous].kind,
SyntaxKind::LeftBrace
| SyntaxKind::RightBrace
| SyntaxKind::SassIndent
| SyntaxKind::SassDedent
| SyntaxKind::Semicolon
| SyntaxKind::SassOptionalSemicolon
) {
break;
}
index = previous;
}
index
}
fn parse_dialect_at_rule(&mut self) {
let Some(spec) = self.current_dialect_at_rule_spec() else {
self.parse_at_rule();
return;
};
self.builder
.start_node(self.current_dialect_at_rule_node_kind(spec));
if self.current_kind() == Some(SyntaxKind::AtKeyword) {
self.token_current();
}
if matches!(
spec.node_kind,
SyntaxKind::ScssUseRule | SyntaxKind::ScssForwardRule
) {
self.parse_scss_module_prelude(spec.node_kind);
}
if is_scss_control_rule_kind(spec.node_kind)
&& !self.current_scss_control_prelude_is_valid(spec.node_kind)
{
self.error_at_current(
ParseErrorCode::ExpectedValue,
"invalid SCSS control prelude",
);
}
while !self.at_end() {
match self.current_kind() {
Some(kind) if is_statement_end(kind) => {
self.token_current();
break;
}
Some(SyntaxKind::LeftBrace) => {
match spec.block_kind {
AtRuleBlockKind::GroupRuleList => self.parse_group_at_rule_block(),
AtRuleBlockKind::DeclarationList => self.parse_declaration_block(),
AtRuleBlockKind::Keyframes => self.parse_keyframes_block(),
AtRuleBlockKind::Raw => self.consume_balanced_block(),
}
break;
}
Some(SyntaxKind::SassIndent) => {
self.parse_sass_indented_at_rule_block(spec.block_kind);
break;
}
Some(_) => self.token_current(),
None => break,
}
}
self.builder.finish_node();
}
fn parse_scss_module_prelude(&mut self, node_kind: SyntaxKind) {
self.validate_scss_module_prelude(node_kind);
while !self.at_end() {
match self.current_kind() {
Some(kind)
if is_statement_end(kind)
|| kind == SyntaxKind::LeftBrace
|| kind == SyntaxKind::SassIndent =>
{
break;
}
Some(SyntaxKind::Ident | SyntaxKind::KeywordWith)
if self.current_text() == Some("with")
&& self
.non_trivia_token_from(self.position + 1)
.is_some_and(|(_, kind)| kind == SyntaxKind::LeftParen) =>
{
self.parse_scss_module_config()
}
Some(kind) if is_interpolation_start(kind) => self.parse_interpolation(
kind,
&[
SyntaxKind::Semicolon,
SyntaxKind::SassOptionalSemicolon,
SyntaxKind::LeftBrace,
SyntaxKind::SassIndent,
],
),
Some(_) => self.token_current(),
None => break,
}
}
}
fn validate_scss_module_prelude(&mut self, node_kind: SyntaxKind) {
let recovery = [
SyntaxKind::Semicolon,
SyntaxKind::SassOptionalSemicolon,
SyntaxKind::LeftBrace,
SyntaxKind::SassIndent,
];
let Some((source_index, source_kind)) = self.non_trivia_token_from(self.position) else {
self.error_at_current(ParseErrorCode::ExpectedValue, "expected SCSS module source");
return;
};
if recovery.contains(&source_kind) || !is_scss_module_source_token(source_kind) {
let range = self
.tokens
.get(source_index)
.map(|token| token.range)
.unwrap_or_else(|| self.current_range());
self.errors.push(ParseError {
code: ParseErrorCode::ExpectedValue,
range,
message: "expected SCSS module source",
});
}
let mut index = source_index;
while let Some(token) = self.tokens.get(index).copied() {
if recovery.contains(&token.kind) {
break;
}
if token.kind == SyntaxKind::Ident {
if token.text.eq_ignore_ascii_case("as") {
let next_kind = self.non_trivia_token_from(index + 1).map(|(_, kind)| kind);
if next_kind.is_none_or(|kind| {
recovery.contains(&kind) || !is_scss_module_namespace_token(kind)
}) {
self.errors.push(ParseError {
code: ParseErrorCode::ExpectedValue,
range: token.range,
message: "expected SCSS module namespace",
});
}
} else if token.text.eq_ignore_ascii_case("with") {
let next_kind = self.non_trivia_token_from(index + 1).map(|(_, kind)| kind);
if next_kind != Some(SyntaxKind::LeftParen) {
self.errors.push(ParseError {
code: ParseErrorCode::ExpectedValue,
range: token.range,
message: "expected SCSS module configuration",
});
}
} else if matches_ignore_ascii_case(token.text, &["show", "hide"]) {
if node_kind != SyntaxKind::ScssForwardRule {
self.errors.push(ParseError {
code: ParseErrorCode::UnexpectedCharacter,
range: token.range,
message: "unexpected SCSS module visibility clause",
});
}
let next_kind = self.non_trivia_token_from(index + 1).map(|(_, kind)| kind);
if next_kind.is_none_or(|kind| {
recovery.contains(&kind) || !is_scss_module_visibility_name_token(kind)
}) {
self.errors.push(ParseError {
code: ParseErrorCode::ExpectedValue,
range: token.range,
message: "expected SCSS module visibility name",
});
}
}
}
index += 1;
}
}
fn current_scss_control_prelude_is_valid(&self, node_kind: SyntaxKind) -> bool {
let recovery = [
SyntaxKind::LeftBrace,
SyntaxKind::SassIndent,
SyntaxKind::Semicolon,
SyntaxKind::SassOptionalSemicolon,
SyntaxKind::RightBrace,
SyntaxKind::SassDedent,
];
match node_kind {
SyntaxKind::ScssControlIf | SyntaxKind::ScssControlWhile => self
.non_trivia_token_from(self.position)
.is_some_and(|(_, kind)| !recovery.contains(&kind)),
SyntaxKind::ScssControlFor => {
self.non_trivia_token_from(self.position)
.is_some_and(|(_, kind)| kind == SyntaxKind::ScssVariable)
&& self.find_text_before_recovery("from", &recovery)
&& (self.find_text_before_recovery("to", &recovery)
|| self.find_text_before_recovery("through", &recovery))
}
SyntaxKind::ScssControlEach => {
self.non_trivia_token_from(self.position)
.is_some_and(|(_, kind)| kind == SyntaxKind::ScssVariable)
&& self.find_text_before_recovery("in", &recovery)
}
SyntaxKind::ScssControlElse => true,
_ => true,
}
}
fn parse_scss_module_config(&mut self) {
let has_balanced_config = self.current_scss_module_config_has_balanced_parens();
self.builder.start_node(if has_balanced_config {
SyntaxKind::ScssModuleConfig
} else {
SyntaxKind::BogusScssModuleConfig
});
self.token_current();
self.eat_trivia();
if self.current_kind() == Some(SyntaxKind::LeftParen) {
self.parse_balanced_parenthesized_prelude_until(
None,
&[
SyntaxKind::LeftBrace,
SyntaxKind::SassIndent,
SyntaxKind::Semicolon,
SyntaxKind::SassOptionalSemicolon,
],
);
}
self.builder.finish_node();
}
fn parse_css_module_value_rule(&mut self) {
let has_name = self
.non_trivia_token_from(self.position + 1)
.and_then(|(index, kind)| {
self.tokens
.get(index)
.map(|token| (kind, token.text != "from"))
})
.is_some_and(|(kind, allowed_name)| {
allowed_name && matches!(kind, SyntaxKind::Ident | SyntaxKind::CustomPropertyName)
});
let has_from = self.find_text_before_recovery(
"from",
&[
SyntaxKind::Semicolon,
SyntaxKind::SassOptionalSemicolon,
SyntaxKind::LeftBrace,
SyntaxKind::SassIndent,
],
);
let has_colon = self.find_before_recovery(
SyntaxKind::Colon,
&[
SyntaxKind::Semicolon,
SyntaxKind::SassOptionalSemicolon,
SyntaxKind::LeftBrace,
SyntaxKind::SassIndent,
],
);
let kind = if !has_name {
SyntaxKind::BogusCssModuleBlock
} else if has_from && !has_colon {
SyntaxKind::CssModuleImportBlock
} else {
SyntaxKind::CssModuleExportBlock
};
self.builder.start_node(kind);
self.token_current();
if !has_name {
self.error_at_current(
ParseErrorCode::UnexpectedCharacter,
"expected CSS Modules @value name",
);
}
if has_colon {
self.parse_css_module_value_export();
} else {
self.parse_css_module_value_import_or_statement();
}
if self.current_kind().is_some_and(is_statement_end) {
self.token_current();
}
self.builder.finish_node();
}
fn parse_css_module_value_export(&mut self) {
self.parse_css_module_token_definitions_until(&[
SyntaxKind::Colon,
SyntaxKind::Semicolon,
SyntaxKind::SassOptionalSemicolon,
]);
if self.current_kind() == Some(SyntaxKind::Colon) {
self.token_current();
self.builder.start_node(SyntaxKind::Value);
self.parse_css_module_token_references_until(&[
SyntaxKind::Semicolon,
SyntaxKind::SassOptionalSemicolon,
]);
self.builder.finish_node();
}
}
fn parse_css_module_value_import_or_statement(&mut self) {
self.parse_css_module_token_definitions_until(&[
SyntaxKind::Semicolon,
SyntaxKind::SassOptionalSemicolon,
]);
}
fn parse_css_module_token_definitions_until(&mut self, recovery: &[SyntaxKind]) {
while !self.at_end() {
match self.current_kind() {
Some(kind) if recovery.contains(&kind) => break,
Some(SyntaxKind::Ident) if self.current_text() == Some("from") => {
self.parse_css_module_from_clause(recovery);
break;
}
Some(SyntaxKind::Ident | SyntaxKind::CustomPropertyName) => {
self.builder.start_node(SyntaxKind::TokenDefinition);
self.token_current();
self.builder.finish_node();
}
Some(_) => self.token_current(),
None => break,
}
}
}
fn parse_css_module_token_references_until(&mut self, recovery: &[SyntaxKind]) {
while !self.at_end() {
self.eat_value_trivia();
match self.current_kind() {
Some(kind) if recovery.contains(&kind) => break,
Some(SyntaxKind::Ident | SyntaxKind::CustomPropertyName) => {
self.builder.start_node(SyntaxKind::TokenReference);
self.token_current();
self.builder.finish_node();
}
Some(kind) if is_interpolation_start(kind) => {
self.parse_interpolation(kind, recovery)
}
Some(_) => self.token_current(),
None => break,
}
}
}
fn parse_less_mixin_header(&mut self) {
self.builder.start_node(SyntaxKind::SelectorList);
self.parse_until_recovery_with_optional_less_guard(&[SyntaxKind::LeftBrace]);
self.builder.finish_node();
}
fn parse_less_mixin_call(&mut self) {
self.builder.start_node(SyntaxKind::LessMixinCall);
self.parse_until_recovery_with_optional_less_guard(&[
SyntaxKind::Semicolon,
SyntaxKind::SassOptionalSemicolon,
SyntaxKind::RightBrace,
SyntaxKind::SassDedent,
]);
if self.current_kind().is_some_and(is_statement_end) {
self.token_current();
}
self.builder.finish_node();
}
fn parse_less_namespace_access(&mut self) {
self.builder.start_node(SyntaxKind::LessNamespaceAccess);
while !self.at_end() {
match self.current_kind() {
Some(
SyntaxKind::Semicolon
| SyntaxKind::SassOptionalSemicolon
| SyntaxKind::RightBrace
| SyntaxKind::SassDedent
| SyntaxKind::LeftBrace
| SyntaxKind::SassIndent,
) => break,
Some(_) if self.current_starts_less_mixin_call() => {
self.parse_less_mixin_call();
break;
}
Some(_) => self.token_current(),
None => break,
}
}
if self.current_kind().is_some_and(is_statement_end) {
self.token_current();
}
self.builder.finish_node();
}
fn parse_until_recovery_with_optional_less_guard(&mut self, recovery: &[SyntaxKind]) {
let mut guard_open = false;
while !self.at_end() {
match self.current_kind() {
Some(kind) if recovery.contains(&kind) => break,
Some(SyntaxKind::Ident) if self.current_text() == Some("when") && !guard_open => {
self.builder.start_node(
if self.current_less_guard_has_condition_before(recovery) {
SyntaxKind::LessMixinGuard
} else {
SyntaxKind::BogusLessGuard
},
);
guard_open = true;
self.token_current();
}
Some(_) => self.token_current(),
None => break,
}
}
if guard_open {
self.builder.finish_node();
}
}
fn parse_value_until(&mut self, recovery: &[SyntaxKind]) {
while !self.at_end() {
self.eat_value_trivia();
if matches!(self.current_kind(), Some(kind) if recovery.contains(&kind)) {
break;
}
if self.at_end() {
break;
}
self.parse_value_expression(0, recovery);
}
}
fn parse_value_or_value_list_until(&mut self, recovery: &[SyntaxKind]) {
if self.current_value_has_top_level_comma_before(recovery) {
self.parse_value_list_until(recovery);
} else {
self.parse_value_until(recovery);
}
}
fn parse_declaration_value_or_value_list_until(&mut self, recovery: &[SyntaxKind]) {
if self.current_value_has_top_level_comma_before(recovery) {
self.parse_declaration_value_list_until(recovery);
} else {
self.parse_declaration_value_until(recovery);
}
}
fn parse_declaration_value_until(&mut self, recovery: &[SyntaxKind]) {
let mut saw_value = false;
while !self.at_end() {
self.eat_value_trivia();
if matches!(self.current_kind(), Some(kind) if recovery.contains(&kind)) {
break;
}
if saw_value && self.current_starts_missing_semicolon_declaration(recovery) {
self.error_at_current(
ParseErrorCode::UnexpectedCharacter,
"expected semicolon between declarations",
);
break;
}
if self.at_end() {
break;
}
self.parse_value_expression(0, recovery);
saw_value = true;
}
}
fn parse_declaration_value_list_until(&mut self, recovery: &[SyntaxKind]) {
self.builder
.start_node(if self.current_value_list_is_bogus(recovery) {
SyntaxKind::BogusValueList
} else {
SyntaxKind::ValueList
});
let item_recovery = value_list_item_recovery(recovery);
let mut saw_item = false;
while !self.at_end() {
self.eat_value_trivia();
match self.current_kind() {
Some(kind) if recovery.contains(&kind) => break,
Some(SyntaxKind::Comma) => self.token_current(),
Some(_)
if saw_item && self.current_starts_missing_semicolon_declaration(recovery) =>
{
self.error_at_current(
ParseErrorCode::UnexpectedCharacter,
"expected semicolon between declarations",
);
break;
}
Some(_) => {
self.parse_value_expression(0, &item_recovery);
saw_item = true;
}
None => break,
}
}
self.builder.finish_node();
}
fn parse_value_list_until(&mut self, recovery: &[SyntaxKind]) {
self.builder
.start_node(if self.current_value_list_is_bogus(recovery) {
SyntaxKind::BogusValueList
} else {
SyntaxKind::ValueList
});
let item_recovery = value_list_item_recovery(recovery);
while !self.at_end() {
self.eat_value_trivia();
match self.current_kind() {
Some(kind) if recovery.contains(&kind) => break,
Some(SyntaxKind::Comma) => self.token_current(),
Some(_) => self.parse_value_expression(0, &item_recovery),
None => break,
}
}
self.builder.finish_node();
}
fn parse_component_value(&mut self, recovery: &[SyntaxKind]) {
self.builder.start_node(SyntaxKind::ComponentValue);
self.parse_component_value_inner(recovery);
self.builder.finish_node();
}
fn parse_component_value_list_until(&mut self, recovery: &[SyntaxKind]) {
self.builder.start_node(SyntaxKind::ComponentValueList);
while !self.at_end() {
self.eat_value_trivia();
match self.current_kind() {
Some(kind) if recovery.contains(&kind) => break,
Some(_) => self.parse_component_value(recovery),
None => break,
}
}
self.builder.finish_node();
}
fn parse_comma_separated_component_value_list_until(&mut self, recovery: &[SyntaxKind]) {
self.builder
.start_node(SyntaxKind::CommaSeparatedComponentValueList);
let item_recovery = comma_separated_component_value_list_item_recovery(recovery);
while !self.at_end() {
self.eat_value_trivia();
match self.current_kind() {
Some(kind) if recovery.contains(&kind) => break,
Some(SyntaxKind::Comma) => self.token_current(),
Some(_) => self.parse_component_value(&item_recovery),
None => break,
}
}
self.builder.finish_node();
}
fn parse_component_value_inner(&mut self, recovery: &[SyntaxKind]) {
self.eat_value_trivia();
match self.current_kind() {
Some(kind) if recovery.contains(&kind) => {
self.empty_bogus_node(
SyntaxKind::BogusValue,
ParseErrorCode::ExpectedValue,
"expected component value",
);
}
Some(SyntaxKind::LeftBrace | SyntaxKind::LeftBracket | SyntaxKind::LeftParen) => {
self.parse_simple_block(recovery)
}
Some(SyntaxKind::Ident) if self.next_kind() == Some(SyntaxKind::LeftParen) => {
self.parse_function_call(recovery)
}
Some(kind) if is_component_value_atom_start(kind) => self.parse_value_prefix(recovery),
Some(_) => self.token_current(),
None => {
self.empty_bogus_node(
SyntaxKind::BogusValue,
ParseErrorCode::ExpectedValue,
"expected component value",
);
}
}
}
fn parse_simple_block_entry_point(&mut self, recovery: &[SyntaxKind]) {
self.eat_value_trivia();
match self.current_kind() {
Some(SyntaxKind::LeftBrace | SyntaxKind::LeftBracket | SyntaxKind::LeftParen) => {
self.parse_simple_block(recovery)
}
Some(_) | None => {
self.empty_bogus_node(
SyntaxKind::BogusSimpleBlock,
ParseErrorCode::ExpectedValue,
"expected simple block",
);
}
}
}
fn parse_simple_block(&mut self, recovery: &[SyntaxKind]) {
let Some(open_kind) = self.current_kind() else {
self.empty_bogus_node(
SyntaxKind::BogusSimpleBlock,
ParseErrorCode::ExpectedValue,
"expected simple block",
);
return;
};
let Some(close_kind) = matching_simple_block_close(open_kind) else {
self.empty_bogus_node(
SyntaxKind::BogusSimpleBlock,
ParseErrorCode::ExpectedValue,
"expected simple block",
);
return;
};
let block_kind = if self.current_simple_block_has_matching_close(recovery) {
SyntaxKind::SimpleBlock
} else {
SyntaxKind::BogusSimpleBlock
};
self.builder.start_node(block_kind);
self.token_current();
let block_recovery = simple_block_recovery(close_kind, recovery);
while !self.at_end() {
self.eat_value_trivia();
match self.current_kind() {
Some(kind) if kind == close_kind => break,
Some(kind) if recovery.contains(&kind) => break,
Some(_) => self.parse_component_value(&block_recovery),
None => break,
}
}
if self.current_kind() == Some(close_kind) {
self.token_current();
} else {
self.error_at_current(
ParseErrorCode::UnexpectedCharacter,
"unterminated simple block",
);
}
self.builder.finish_node();
}
fn parse_value_expression(&mut self, min_binding_power: u8, recovery: &[SyntaxKind]) {
self.eat_value_trivia();
let checkpoint = self.builder.checkpoint();
self.parse_value_prefix(recovery);
loop {
self.eat_value_trivia();
let Some(operator) = self.current_kind() else {
break;
};
if recovery.contains(&operator) {
break;
}
let Some((left_binding_power, right_binding_power)) = infix_binding_power(operator)
else {
break;
};
if left_binding_power < min_binding_power {
break;
}
self.builder
.start_node_at(checkpoint, SyntaxKind::BinaryExpression);
self.token_current();
self.parse_value_expression(right_binding_power, recovery);
self.builder.finish_node();
}
}
fn parse_value_prefix(&mut self, recovery: &[SyntaxKind]) {
match self.current_kind() {
Some(SyntaxKind::Plus | SyntaxKind::Minus) => {
self.builder.start_node(SyntaxKind::UnaryExpression);
self.token_current();
self.parse_value_expression(5, recovery);
self.builder.finish_node();
}
Some(SyntaxKind::Ident)
if self
.current_text()
.is_some_and(|text| text.eq_ignore_ascii_case("url"))
&& self.next_kind() == Some(SyntaxKind::LeftParen) =>
{
self.builder.start_node(SyntaxKind::UrlValue);
self.parse_function_call(recovery);
self.builder.finish_node();
}
Some(SyntaxKind::Ident) if self.next_kind() == Some(SyntaxKind::LeftParen) => {
self.parse_function_call(recovery)
}
Some(SyntaxKind::Number) => {
self.builder.start_node(SyntaxKind::NumberValue);
self.token_current();
self.builder.finish_node();
}
Some(SyntaxKind::Percentage) => {
self.builder.start_node(SyntaxKind::PercentageValue);
self.token_current();
self.builder.finish_node();
}
Some(SyntaxKind::Dimension) => {
self.builder.start_node(SyntaxKind::DimensionValue);
self.token_current();
self.builder.finish_node();
}
Some(SyntaxKind::Ident | SyntaxKind::CustomPropertyName) => {
self.builder.start_node(SyntaxKind::IdentifierValue);
self.token_current();
self.builder.finish_node();
}
Some(SyntaxKind::String | SyntaxKind::LessEscapedString) => {
self.builder.start_node(SyntaxKind::StringValue);
self.token_current();
self.builder.finish_node();
}
Some(SyntaxKind::UnicodeRange) => {
self.builder.start_node(SyntaxKind::UnicodeRangeValue);
self.token_current();
self.builder.finish_node();
}
Some(SyntaxKind::Hash) => {
self.builder.start_node(SyntaxKind::ColorValue);
self.token_current();
self.builder.finish_node();
}
Some(SyntaxKind::Url) => {
self.builder.start_node(SyntaxKind::UrlValue);
self.token_current();
self.builder.finish_node();
}
Some(SyntaxKind::BadUrl) => {
self.builder.start_node(SyntaxKind::BogusValue);
self.token_current();
self.builder.finish_node();
}
Some(SyntaxKind::BadString) => {
self.builder.start_node(SyntaxKind::BogusValue);
self.token_current();
self.builder.finish_node();
}
Some(SyntaxKind::Important) => {
self.builder.start_node(SyntaxKind::ImportantAnnotation);
self.token_current();
self.builder.finish_node();
}
Some(SyntaxKind::Delim) if self.current_split_important_annotation() => {
self.parse_split_important_annotation()
}
Some(SyntaxKind::Delim) if self.current_scss_variable_flag_annotation() => {
self.parse_scss_variable_flag_annotation()
}
Some(kind) if is_interpolation_start(kind) => self.parse_interpolation(kind, recovery),
Some(SyntaxKind::ScssVariable) => {
self.builder.start_node(SyntaxKind::ScssVariableReference);
self.token_current();
self.builder.finish_node();
}
Some(SyntaxKind::LessVariable) => {
self.builder.start_node(SyntaxKind::LessVariableReference);
self.token_current();
self.builder.finish_node();
}
Some(SyntaxKind::LessPropertyVariableToken) => {
self.builder.start_node(SyntaxKind::LessPropertyVariable);
self.token_current();
self.builder.finish_node();
}
Some(SyntaxKind::LeftBrace) => self.parse_simple_block(recovery),
Some(SyntaxKind::LeftParen) => self.parse_parenthesized_expression(recovery),
Some(SyntaxKind::LeftBracket) => self.parse_bracketed_value(recovery),
Some(kind) if recovery.contains(&kind) => {
self.empty_bogus_node(
SyntaxKind::BogusValue,
ParseErrorCode::ExpectedValue,
"expected value",
);
}
Some(SyntaxKind::Delim) => {
self.builder.start_node(SyntaxKind::BogusToken);
self.token_current();
self.builder.finish_node();
}
Some(_) => {
self.builder.start_node(SyntaxKind::BogusValue);
self.error_at_current(ParseErrorCode::ExpectedValue, "expected value");
self.token_current();
self.builder.finish_node();
}
None => {
self.empty_bogus_node(
SyntaxKind::BogusValue,
ParseErrorCode::ExpectedValue,
"expected value",
);
}
}
}
fn parse_split_important_annotation(&mut self) {
self.builder.start_node(SyntaxKind::ImportantAnnotation);
self.token_current();
self.eat_value_trivia();
if self
.current_text()
.is_some_and(|text| text.eq_ignore_ascii_case("important"))
{
self.token_current();
}
self.builder.finish_node();
}
fn parse_scss_variable_flag_annotation(&mut self) {
self.builder.start_node(SyntaxKind::ScssVariableFlag);
self.token_current();
self.eat_value_trivia();
self.token_current();
self.builder.finish_node();
}
fn eat_value_trivia(&mut self) {
while matches!(self.current_kind(), Some(kind) if kind.is_trivia()) {
self.token_current();
}
}
fn parse_function_call(&mut self, recovery: &[SyntaxKind]) {
let function_name = self.current_text().map(str::to_owned);
let function_range = self.current_range();
let argument_count = self.current_function_top_level_argument_count_before(recovery);
let has_empty_argument_slot =
self.current_function_has_empty_top_level_argument_slot_before(recovery);
let argument_head = self.current_function_first_argument_token_before(recovery);
let specialized_kind = function_name.as_deref().and_then(specialized_function_kind);
let closed = self.current_function_has_closing_paren_before(recovery);
let function_kind = if closed {
SyntaxKind::FunctionCall
} else {
SyntaxKind::BogusFunctionCall
};
let arguments_kind = if closed {
SyntaxKind::FunctionArguments
} else {
SyntaxKind::BogusFunctionArguments
};
self.builder.start_node(function_kind);
if let Some(kind) = specialized_kind {
self.builder.start_node(kind);
}
self.token_current();
if self.current_kind() == Some(SyntaxKind::LeftParen) {
self.token_current();
self.builder.start_node(arguments_kind);
let argument_recovery = function_argument_recovery(recovery);
self.parse_value_or_value_list_until(&argument_recovery);
self.builder.finish_node();
if self.current_kind() == Some(SyntaxKind::RightParen) {
self.token_current();
} else {
self.error_at_current(
ParseErrorCode::UnexpectedCharacter,
"unterminated function call",
);
}
}
if let Some(function_name) = function_name {
if let Some(argument_count) = argument_count {
self.validate_function_argument_count(
&function_name,
argument_count,
function_range,
);
}
if let Some(true) = has_empty_argument_slot {
self.validate_function_argument_slots(&function_name, function_range);
}
self.validate_function_argument_head(&function_name, argument_head, function_range);
}
if specialized_kind.is_some() {
self.builder.finish_node();
}
self.builder.finish_node();
}
fn current_function_top_level_argument_count_before(
&self,
recovery: &[SyntaxKind],
) -> Option<usize> {
if self.next_kind() != Some(SyntaxKind::LeftParen) {
return None;
}
let mut index = self.position + 2;
let mut depth = 0usize;
let mut comma_count = 0usize;
let mut saw_argument = false;
while let Some(token) = self.tokens.get(index) {
match token.kind {
kind if depth == 0 && recovery.contains(&kind) => return None,
SyntaxKind::RightParen if depth == 0 => {
return Some(if saw_argument { comma_count + 1 } else { 0 });
}
SyntaxKind::Comma if depth == 0 => {
comma_count += 1;
saw_argument = false;
}
kind if kind.is_trivia() => {}
SyntaxKind::LeftBrace | SyntaxKind::LeftBracket | SyntaxKind::LeftParen => {
depth += 1;
saw_argument = true;
}
SyntaxKind::RightBrace | SyntaxKind::RightBracket | SyntaxKind::RightParen => {
depth = depth.saturating_sub(1);
saw_argument = true;
}
_ => saw_argument = true,
}
index += 1;
}
None
}
fn current_function_has_empty_top_level_argument_slot_before(
&self,
recovery: &[SyntaxKind],
) -> Option<bool> {
if self.next_kind() != Some(SyntaxKind::LeftParen) {
return None;
}
let mut index = self.position + 2;
let mut depth = 0usize;
let mut expecting_argument = true;
let mut saw_argument = false;
while let Some(token) = self.tokens.get(index) {
match token.kind {
kind if depth == 0 && recovery.contains(&kind) => return None,
SyntaxKind::RightParen if depth == 0 => {
return Some(expecting_argument && saw_argument);
}
SyntaxKind::Comma if depth == 0 => {
if expecting_argument {
return Some(true);
}
expecting_argument = true;
}
kind if kind.is_trivia() => {}
SyntaxKind::LeftBrace | SyntaxKind::LeftBracket | SyntaxKind::LeftParen => {
depth += 1;
expecting_argument = false;
saw_argument = true;
}
SyntaxKind::RightBrace | SyntaxKind::RightBracket | SyntaxKind::RightParen => {
depth = depth.saturating_sub(1);
expecting_argument = false;
saw_argument = true;
}
_ => {
expecting_argument = false;
saw_argument = true;
}
}
index += 1;
}
None
}
fn current_function_first_argument_token_before(
&self,
recovery: &[SyntaxKind],
) -> Option<Token<'text>> {
if self.next_kind() != Some(SyntaxKind::LeftParen) {
return None;
}
let mut index = self.position + 2;
while let Some(token) = self.tokens.get(index).copied() {
match token.kind {
kind if recovery.contains(&kind) => return None,
SyntaxKind::RightParen => return None,
kind if kind.is_trivia() => {}
_ => return Some(token),
}
index += 1;
}
None
}
fn validate_function_argument_count(
&mut self,
function_name: &str,
argument_count: usize,
range: TextRange,
) {
if function_argument_count_is_valid(function_name, argument_count) {
return;
}
self.errors.push(ParseError {
code: ParseErrorCode::ExpectedValue,
range,
message: "invalid function argument count",
});
}
fn validate_function_argument_slots(&mut self, function_name: &str, range: TextRange) {
if !function_requires_filled_top_level_arguments(function_name) {
return;
}
self.errors.push(ParseError {
code: ParseErrorCode::ExpectedValue,
range,
message: "empty function argument",
});
}
fn validate_function_argument_head(
&mut self,
function_name: &str,
argument_head: Option<Token<'text>>,
range: TextRange,
) {
let head_kind = argument_head.map(|token| token.kind);
let valid = if function_name.eq_ignore_ascii_case("var") {
matches!(head_kind, Some(SyntaxKind::CustomPropertyName))
|| head_kind.is_some_and(is_dynamic_function_argument_head)
} else if function_name.eq_ignore_ascii_case("env") {
matches!(
head_kind,
Some(SyntaxKind::Ident | SyntaxKind::CustomPropertyName)
) || head_kind.is_some_and(is_dynamic_function_argument_head)
} else if function_name.eq_ignore_ascii_case("attr") {
matches!(head_kind, Some(SyntaxKind::Ident))
|| head_kind.is_some_and(is_dynamic_function_argument_head)
} else if function_name.eq_ignore_ascii_case("color-mix") {
argument_head.is_some_and(|token| token.text.eq_ignore_ascii_case("in"))
|| head_kind.is_some_and(is_dynamic_function_argument_head)
} else {
true
};
if valid {
return;
}
self.errors.push(ParseError {
code: ParseErrorCode::ExpectedValue,
range,
message: "invalid function argument head",
});
}
fn parse_bracketed_value(&mut self, recovery: &[SyntaxKind]) {
let closed = self.current_bracketed_value_has_closing_bracket_before(recovery);
self.builder.start_node(if closed {
SyntaxKind::BracketedValue
} else {
SyntaxKind::BogusBracketedValue
});
self.token_current();
let bracket_recovery = bracketed_value_recovery(recovery);
self.parse_value_until(&bracket_recovery);
if self.current_kind() == Some(SyntaxKind::RightBracket) {
self.token_current();
} else {
self.error_at_current(
ParseErrorCode::UnexpectedCharacter,
"unterminated bracketed value",
);
}
self.builder.finish_node();
}
fn parse_parenthesized_expression(&mut self, recovery: &[SyntaxKind]) {
self.builder.start_node(SyntaxKind::ParenthesizedExpression);
self.token_current();
let paren_recovery = function_argument_recovery(recovery);
self.parse_value_until(&paren_recovery);
if self.current_kind() == Some(SyntaxKind::RightParen) {
self.token_current();
}
self.builder.finish_node();
}
fn parse_at_rule(&mut self) {
let spec = self.current_text().and_then(at_rule_spec);
let at_rule_kind = if spec.is_none() && self.current_text() == Some("@") {
SyntaxKind::BogusAtRule
} else {
SyntaxKind::AtRule
};
self.builder.start_node(at_rule_kind);
if at_rule_kind == SyntaxKind::BogusAtRule {
self.error_at_current(ParseErrorCode::UnexpectedCharacter, "expected at-rule name");
}
if let Some(spec) = spec {
self.builder.start_node(spec.node_kind);
}
if self.current_kind() == Some(SyntaxKind::AtKeyword) {
self.token_current();
}
if let Some(spec) = spec {
self.parse_at_rule_prelude(spec.node_kind);
} else {
self.consume_at_rule_prelude_tokens();
}
while !self.at_end() {
match self.current_kind() {
Some(kind) if is_statement_end(kind) => {
self.token_current();
break;
}
Some(SyntaxKind::LeftBrace) => {
match spec
.map(|spec| spec.block_kind)
.unwrap_or(AtRuleBlockKind::Raw)
{
AtRuleBlockKind::GroupRuleList => self.parse_group_at_rule_block(),
AtRuleBlockKind::DeclarationList => self.parse_declaration_block(),
AtRuleBlockKind::Keyframes => self.parse_keyframes_block(),
AtRuleBlockKind::Raw => self.consume_balanced_block(),
}
break;
}
Some(SyntaxKind::SassIndent) => {
self.parse_sass_indented_at_rule_block(
spec.map(|spec| spec.block_kind)
.unwrap_or(AtRuleBlockKind::Raw),
);
break;
}
Some(_) => self.token_current(),
None => break,
}
}
if spec.is_some() {
self.builder.finish_node();
}
self.builder.finish_node();
}
fn parse_at_rule_prelude(&mut self, node_kind: SyntaxKind) {
match node_kind {
SyntaxKind::MediaRule => self.parse_media_query_list(),
SyntaxKind::SupportsRule => self.parse_supports_rule_prelude(),
SyntaxKind::ContainerRule => self.parse_container_rule_prelude(),
SyntaxKind::ImportRule => self.parse_import_prelude(),
SyntaxKind::CharsetRule => self.parse_charset_rule_prelude(),
SyntaxKind::NamespaceRule => self.parse_namespace_rule_prelude(),
SyntaxKind::KeyframesRule => self.parse_keyframes_rule_prelude(),
SyntaxKind::PageRule => self.parse_page_rule_prelude(),
SyntaxKind::FontFaceRule
| SyntaxKind::StartingStyleRule
| SyntaxKind::PageMarginRule
| SyntaxKind::FontFeatureValuesStylisticRule
| SyntaxKind::FontFeatureValuesStylesetRule
| SyntaxKind::FontFeatureValuesCharacterVariantRule
| SyntaxKind::FontFeatureValuesSwashRule
| SyntaxKind::FontFeatureValuesOrnamentsRule
| SyntaxKind::FontFeatureValuesAnnotationRule
| SyntaxKind::FontFeatureValuesHistoricalFormsRule
| SyntaxKind::ViewTransitionRule => {
self.parse_empty_at_rule_prelude("unexpected at-rule prelude")
}
SyntaxKind::PropertyRule => self.parse_named_at_rule_prelude(
at_rule_prelude_head_is_custom_property_name,
"invalid @property name",
),
SyntaxKind::FontPaletteValuesRule
| SyntaxKind::ColorProfileRule
| SyntaxKind::PositionTryRule => self.parse_named_at_rule_prelude(
at_rule_prelude_head_is_custom_property_name,
"invalid at-rule custom property name",
),
SyntaxKind::CustomMediaRule => self.parse_custom_media_rule_prelude(),
SyntaxKind::CounterStyleRule => self.parse_named_at_rule_prelude(
at_rule_prelude_head_is_custom_ident,
"invalid @counter-style name",
),
SyntaxKind::FontFeatureValuesRule => self.parse_font_feature_values_prelude(),
SyntaxKind::LayerRule => self.parse_layer_rule_prelude(),
SyntaxKind::ScopeRule => self.parse_scope_rule_prelude(),
_ => self.consume_at_rule_prelude_tokens(),
}
}
fn parse_media_query_list(&mut self) {
self.builder.start_node(SyntaxKind::MediaQueryList);
let mut saw_query = false;
let mut expecting_query = true;
while !self.at_end() {
match self.current_kind() {
Some(kind) if is_at_rule_prelude_boundary(kind) => break,
Some(SyntaxKind::Comma) => {
if expecting_query {
self.error_at_current(
ParseErrorCode::ExpectedValue,
"invalid @media prelude",
);
self.builder.start_node(SyntaxKind::BogusMediaQuery);
self.token_current();
self.builder.finish_node();
} else {
self.token_current();
expecting_query = true;
}
}
Some(_) => {
let valid = self.current_media_query_is_valid();
if !valid {
self.error_at_current(
ParseErrorCode::ExpectedValue,
"invalid @media prelude",
);
}
self.parse_media_query(valid);
saw_query = true;
expecting_query = false;
}
None => break,
}
}
if !saw_query || expecting_query {
self.error_at_current(ParseErrorCode::ExpectedValue, "invalid @media prelude");
self.builder.start_node(SyntaxKind::BogusMediaQuery);
self.builder.finish_node();
}
self.builder.finish_node();
}
fn parse_media_query(&mut self, valid: bool) {
self.builder.start_node(if valid {
SyntaxKind::MediaQuery
} else {
SyntaxKind::BogusMediaQuery
});
while !self.at_end() {
match self.current_kind() {
Some(kind) if is_at_rule_prelude_boundary(kind) || kind == SyntaxKind::Comma => {
break;
}
Some(SyntaxKind::LeftParen) => self.parse_balanced_parenthesized_prelude_until(
Some(SyntaxKind::MediaFeature),
&[
SyntaxKind::Comma,
SyntaxKind::LeftBrace,
SyntaxKind::Semicolon,
],
),
Some(kind) if is_interpolation_start(kind) => self.parse_interpolation(
kind,
&[
SyntaxKind::Comma,
SyntaxKind::LeftBrace,
SyntaxKind::Semicolon,
],
),
Some(_) => self.token_current(),
None => break,
}
}
self.builder.finish_node();
}
fn current_media_query_is_valid(&self) -> bool {
let Some((first_index, first_kind)) = self.non_trivia_token_from(self.position) else {
return false;
};
if is_at_rule_prelude_boundary(first_kind) || first_kind == SyntaxKind::Comma {
return false;
}
if !self.current_prelude_parentheses_are_balanced_until(&[
SyntaxKind::Comma,
SyntaxKind::LeftBrace,
SyntaxKind::SassIndent,
SyntaxKind::Semicolon,
SyntaxKind::SassOptionalSemicolon,
]) {
return false;
}
self.media_query_starts_at(first_index, first_kind)
}
fn media_query_starts_at(&self, index: usize, kind: SyntaxKind) -> bool {
match kind {
SyntaxKind::Ident | SyntaxKind::LeftParen => true,
SyntaxKind::KeywordNot | SyntaxKind::KeywordOnly => self
.non_trivia_token_from(index + 1)
.is_some_and(|(_, next_kind)| {
matches!(next_kind, SyntaxKind::Ident | SyntaxKind::LeftParen)
|| is_interpolation_start(next_kind)
}),
kind if is_interpolation_start(kind) => true,
_ => false,
}
}
fn parse_charset_rule_prelude(&mut self) {
if !self.charset_rule_prelude_is_valid() {
self.error_at_current(ParseErrorCode::ExpectedValue, "invalid @charset prelude");
}
self.consume_at_rule_prelude_tokens();
}
fn charset_rule_prelude_is_valid(&self) -> bool {
let Some((source_index, SyntaxKind::String)) = self.non_trivia_token_from(self.position)
else {
return false;
};
self.non_trivia_token_from(source_index + 1)
.is_none_or(|(_, kind)| is_at_rule_prelude_boundary(kind))
}
fn parse_namespace_rule_prelude(&mut self) {
if !self.namespace_rule_prelude_is_valid() {
self.error_at_current(ParseErrorCode::ExpectedValue, "invalid @namespace prelude");
}
self.consume_at_rule_prelude_tokens();
}
fn parse_custom_media_rule_prelude(&mut self) {
self.eat_trivia();
let valid = self.custom_media_rule_prelude_is_valid();
if !valid {
self.error_at_current(
ParseErrorCode::ExpectedValue,
"invalid @custom-media prelude",
);
}
self.builder.start_node(if valid {
SyntaxKind::AtRulePrelude
} else {
SyntaxKind::BogusAtRulePrelude
});
self.consume_at_rule_prelude_tokens_without_wrapping();
self.builder.finish_node();
}
fn custom_media_rule_prelude_is_valid(&self) -> bool {
let Some((name_index, name_kind)) = self.non_trivia_token_from(self.position) else {
return false;
};
if !self.current_prelude_parentheses_are_balanced_until(&[
SyntaxKind::Semicolon,
SyntaxKind::SassOptionalSemicolon,
]) {
return false;
}
let tail = if name_kind == SyntaxKind::CustomPropertyName {
self.non_trivia_token_from(name_index + 1)
} else if is_interpolation_start(name_kind) {
self.non_trivia_token_after_interpolation(name_index, name_kind)
} else {
return false;
};
let Some((tail_index, tail_kind)) = tail else {
return false;
};
if is_at_rule_prelude_boundary(tail_kind) {
return false;
}
self.media_query_starts_at(tail_index, tail_kind)
}
fn namespace_rule_prelude_is_valid(&self) -> bool {
let Some((first_index, first_kind)) = self.non_trivia_token_from(self.position) else {
return false;
};
if self.namespace_source_starts_at(first_index, first_kind) {
return true;
}
if !matches!(
first_kind,
SyntaxKind::Ident | SyntaxKind::CustomPropertyName
) {
return false;
}
self.non_trivia_token_from(first_index + 1)
.is_some_and(|(source_index, source_kind)| {
self.namespace_source_starts_at(source_index, source_kind)
})
}
fn namespace_source_starts_at(&self, index: usize, kind: SyntaxKind) -> bool {
matches!(kind, SyntaxKind::String | SyntaxKind::Url)
|| is_interpolation_start(kind)
|| self.token_starts_url_function(index, kind)
}
fn token_starts_url_function(&self, index: usize, kind: SyntaxKind) -> bool {
kind == SyntaxKind::Ident
&& self
.tokens
.get(index)
.is_some_and(|token| token.text.eq_ignore_ascii_case("url"))
&& self
.non_trivia_token_from(index + 1)
.is_some_and(|(_, next_kind)| next_kind == SyntaxKind::LeftParen)
}
fn parse_keyframes_rule_prelude(&mut self) {
if !self.keyframes_rule_prelude_is_valid() {
self.error_at_current(ParseErrorCode::ExpectedValue, "invalid @keyframes name");
}
self.consume_at_rule_prelude_tokens();
}
fn keyframes_rule_prelude_is_valid(&self) -> bool {
let Some((name_index, name_kind)) = self.non_trivia_token_from(self.position) else {
return false;
};
if is_interpolation_start(name_kind) {
return true;
}
if !matches!(name_kind, SyntaxKind::Ident | SyntaxKind::String) {
return false;
}
self.non_trivia_token_from(name_index + 1)
.is_none_or(|(_, kind)| is_at_rule_prelude_boundary(kind))
}
fn parse_empty_at_rule_prelude(&mut self, message: &'static str) {
self.eat_trivia();
if self
.current_kind()
.is_some_and(|kind| !is_at_rule_prelude_boundary(kind))
{
self.error_at_current(ParseErrorCode::ExpectedValue, message);
self.consume_at_rule_prelude_tokens();
}
}
fn parse_font_feature_values_prelude(&mut self) {
if !self.font_feature_values_prelude_is_valid() {
self.error_at_current(
ParseErrorCode::ExpectedValue,
"invalid @font-feature-values family name",
);
}
self.consume_at_rule_prelude_tokens();
}
fn font_feature_values_prelude_is_valid(&self) -> bool {
self.non_trivia_token_from(self.position)
.is_some_and(|(_, kind)| {
matches!(kind, SyntaxKind::Ident | SyntaxKind::String)
|| is_interpolation_start(kind)
})
}
fn parse_layer_rule_prelude(&mut self) {
self.eat_trivia();
match self.current_kind() {
Some(SyntaxKind::LeftBrace | SyntaxKind::SassIndent) => return,
Some(SyntaxKind::Semicolon | SyntaxKind::SassOptionalSemicolon) | None => {
self.empty_bogus_node(
SyntaxKind::BogusLayerName,
ParseErrorCode::ExpectedValue,
"invalid @layer prelude",
);
return;
}
Some(_) => {}
}
let valid = self.layer_rule_prelude_is_valid();
if !valid {
self.error_at_current(ParseErrorCode::ExpectedValue, "invalid @layer prelude");
}
self.builder.start_node(if valid {
SyntaxKind::LayerName
} else {
SyntaxKind::BogusLayerName
});
self.consume_at_rule_prelude_tokens_without_wrapping();
self.builder.finish_node();
}
fn layer_rule_prelude_is_valid(&self) -> bool {
let mut saw_name = false;
let mut expecting_segment = true;
let mut index = self.position;
while let Some(token) = self.tokens.get(index) {
if token.kind.is_trivia() {
index += 1;
continue;
}
if is_at_rule_prelude_boundary(token.kind) {
return saw_name && !expecting_segment;
}
if is_interpolation_start(token.kind) {
return true;
}
match token.kind {
SyntaxKind::Ident if expecting_segment => {
saw_name = true;
expecting_segment = false;
}
SyntaxKind::Comma if saw_name && !expecting_segment => {
expecting_segment = true;
}
SyntaxKind::Dot if saw_name && !expecting_segment => {
expecting_segment = true;
}
_ => return false,
}
index += 1;
}
saw_name && !expecting_segment
}
fn parse_container_rule_prelude(&mut self) {
self.eat_trivia();
let valid = self.container_rule_prelude_is_valid();
if !valid {
self.error_at_current(ParseErrorCode::ExpectedValue, "invalid @container prelude");
}
self.builder.start_node(if valid {
SyntaxKind::ContainerCondition
} else {
SyntaxKind::BogusContainerCondition
});
self.consume_at_rule_prelude_tokens_without_wrapping();
self.builder.finish_node();
}
fn container_rule_prelude_is_valid(&self) -> bool {
let Some((first_index, first_kind)) = self.non_trivia_token_from(self.position) else {
return false;
};
if is_at_rule_prelude_boundary(first_kind) {
return false;
}
if !self.current_prelude_parentheses_are_balanced_until(&[
SyntaxKind::LeftBrace,
SyntaxKind::SassIndent,
SyntaxKind::Semicolon,
SyntaxKind::SassOptionalSemicolon,
]) {
return false;
}
if self.container_condition_starts_at(first_index, first_kind) {
return true;
}
if first_kind != SyntaxKind::Ident {
return false;
}
self.non_trivia_token_from(first_index + 1).is_some_and(
|(condition_index, condition_kind)| {
self.container_condition_starts_at(condition_index, condition_kind)
},
)
}
fn container_condition_starts_at(&self, index: usize, kind: SyntaxKind) -> bool {
if matches!(kind, SyntaxKind::LeftParen | SyntaxKind::KeywordNot)
|| is_interpolation_start(kind)
{
return true;
}
kind == SyntaxKind::Ident
&& self
.non_trivia_token_from(index + 1)
.is_some_and(|(_, next_kind)| next_kind == SyntaxKind::LeftParen)
}
fn parse_supports_rule_prelude(&mut self) {
self.eat_trivia();
let valid = self.supports_rule_prelude_is_valid();
if !valid {
self.error_at_current(ParseErrorCode::ExpectedValue, "invalid @supports prelude");
}
self.builder.start_node(if valid {
SyntaxKind::SupportsCondition
} else {
SyntaxKind::BogusSupportsCondition
});
self.consume_at_rule_prelude_tokens_without_wrapping();
self.builder.finish_node();
}
fn supports_rule_prelude_is_valid(&self) -> bool {
let Some((first_index, first_kind)) = self.non_trivia_token_from(self.position) else {
return false;
};
if is_at_rule_prelude_boundary(first_kind) {
return false;
}
if !self.current_prelude_parentheses_are_balanced_until(&[
SyntaxKind::LeftBrace,
SyntaxKind::SassIndent,
SyntaxKind::Semicolon,
SyntaxKind::SassOptionalSemicolon,
]) {
return false;
}
self.supports_condition_starts_at(first_index, first_kind)
}
fn supports_condition_starts_at(&self, index: usize, kind: SyntaxKind) -> bool {
if kind == SyntaxKind::KeywordNot {
return self
.non_trivia_token_from(index + 1)
.is_some_and(|(next_index, next_kind)| {
self.supports_condition_starts_at(next_index, next_kind)
});
}
if kind == SyntaxKind::LeftParen || is_interpolation_start(kind) {
return true;
}
kind == SyntaxKind::Ident
&& self
.non_trivia_token_from(index + 1)
.is_some_and(|(_, next_kind)| next_kind == SyntaxKind::LeftParen)
}
fn parse_scope_rule_prelude(&mut self) {
self.eat_trivia();
let valid = self.scope_rule_prelude_is_valid();
if !valid {
self.error_at_current(ParseErrorCode::ExpectedValue, "invalid @scope prelude");
}
self.builder.start_node(if valid {
SyntaxKind::ScopeRange
} else {
SyntaxKind::BogusScopeRange
});
self.consume_at_rule_prelude_tokens_without_wrapping();
self.builder.finish_node();
}
fn scope_rule_prelude_is_valid(&self) -> bool {
let Some((start_index, start_kind)) = self.non_trivia_token_from(self.position) else {
return false;
};
if is_at_rule_prelude_boundary(start_kind) {
return false;
}
if !self.current_prelude_parentheses_are_balanced_until(&[
SyntaxKind::LeftBrace,
SyntaxKind::SassIndent,
SyntaxKind::Semicolon,
SyntaxKind::SassOptionalSemicolon,
]) {
return false;
}
if is_interpolation_start(start_kind) {
return true;
}
if start_kind != SyntaxKind::LeftParen {
return false;
}
let Some(start_close_index) = self.parenthesized_prelude_close_index(start_index) else {
return false;
};
let Some((after_start_index, after_start_kind)) =
self.non_trivia_token_from(start_close_index + 1)
else {
return true;
};
if is_at_rule_prelude_boundary(after_start_kind) {
return true;
}
if after_start_kind != SyntaxKind::Ident
|| !self
.tokens
.get(after_start_index)
.is_some_and(|token| token.text.eq_ignore_ascii_case("to"))
{
return false;
}
let Some((end_index, end_kind)) = self.non_trivia_token_from(after_start_index + 1) else {
return false;
};
if is_interpolation_start(end_kind) {
return true;
}
if end_kind != SyntaxKind::LeftParen {
return false;
}
let Some(end_close_index) = self.parenthesized_prelude_close_index(end_index) else {
return false;
};
self.non_trivia_token_from(end_close_index + 1)
.is_none_or(|(_, kind)| is_at_rule_prelude_boundary(kind))
}
fn parenthesized_prelude_close_index(&self, open_index: usize) -> Option<usize> {
let mut depth = 0usize;
for (index, token) in self.tokens.iter().enumerate().skip(open_index) {
match token.kind {
SyntaxKind::LeftParen => depth += 1,
SyntaxKind::RightParen => {
depth = depth.saturating_sub(1);
if depth == 0 {
return Some(index);
}
}
kind if depth == 0 && is_at_rule_prelude_boundary(kind) => return None,
_ => {}
}
}
None
}
fn parse_page_rule_prelude(&mut self) {
self.eat_trivia();
if self.current_kind().is_none_or(is_at_rule_prelude_boundary) {
return;
}
let valid = self.page_rule_prelude_is_valid();
if !valid {
self.error_at_current(ParseErrorCode::ExpectedValue, "invalid @page prelude");
}
self.builder.start_node(if valid {
SyntaxKind::AtRulePrelude
} else {
SyntaxKind::BogusAtRulePrelude
});
self.consume_at_rule_prelude_tokens_without_wrapping();
self.builder.finish_node();
}
fn page_rule_prelude_is_valid(&self) -> bool {
let mut expecting_selector = true;
let mut expecting_pseudo_name = false;
let mut saw_selector = false;
for token in self.tokens.iter().skip(self.position) {
if token.kind.is_trivia() {
continue;
}
if is_at_rule_prelude_boundary(token.kind) {
return saw_selector && !expecting_selector && !expecting_pseudo_name;
}
if is_interpolation_start(token.kind) {
return true;
}
if expecting_pseudo_name {
if token.kind != SyntaxKind::Ident {
return false;
}
saw_selector = true;
expecting_selector = false;
expecting_pseudo_name = false;
continue;
}
match token.kind {
SyntaxKind::Ident if expecting_selector => {
saw_selector = true;
expecting_selector = false;
}
SyntaxKind::Colon => {
expecting_pseudo_name = true;
}
SyntaxKind::Comma if saw_selector && !expecting_selector => {
expecting_selector = true;
}
_ => return false,
}
}
saw_selector && !expecting_selector && !expecting_pseudo_name
}
fn parse_import_prelude(&mut self) {
self.eat_trivia();
if self.dialect == StyleDialect::Less && self.current_kind() == Some(SyntaxKind::LeftParen)
{
self.builder.start_node(SyntaxKind::AtRulePrelude);
self.parse_balanced_parenthesized_prelude(None);
self.builder.finish_node();
self.eat_trivia();
}
if !self.parse_import_source() {
self.parse_bogus_import_prelude();
return;
}
while !self.at_end() {
match self.current_kind() {
Some(kind) if is_at_rule_prelude_boundary(kind) => break,
Some(kind) if kind.is_trivia() => self.token_current(),
Some(SyntaxKind::Ident) if self.current_text() == Some("layer") => {
self.parse_import_layer_tail_node()
}
Some(SyntaxKind::Ident) if self.current_text() == Some("supports") => {
self.parse_import_supports_tail_node()
}
Some(_) => {
self.parse_media_query_list();
break;
}
None => break,
}
}
}
fn parse_import_source(&mut self) -> bool {
match self.current_kind() {
Some(SyntaxKind::Url) => {
self.builder.start_node(SyntaxKind::UrlValue);
self.token_current();
self.builder.finish_node();
true
}
Some(SyntaxKind::Ident)
if self
.current_text()
.is_some_and(|text| text.eq_ignore_ascii_case("url"))
&& self.next_kind() == Some(SyntaxKind::LeftParen) =>
{
self.builder.start_node(SyntaxKind::UrlValue);
self.parse_function_call(&[SyntaxKind::LeftBrace, SyntaxKind::Semicolon]);
self.builder.finish_node();
true
}
Some(SyntaxKind::String) => {
self.token_current();
true
}
Some(kind) if is_interpolation_start(kind) => {
self.parse_interpolation(kind, &[SyntaxKind::LeftBrace, SyntaxKind::Semicolon]);
true
}
Some(_) | None => false,
}
}
fn parse_bogus_import_prelude(&mut self) {
self.builder.start_node(SyntaxKind::BogusAtRulePrelude);
self.error_at_current(ParseErrorCode::ExpectedValue, "invalid @import source");
self.consume_at_rule_prelude_tokens_without_wrapping();
self.builder.finish_node();
}
fn parse_named_at_rule_prelude(
&mut self,
valid_head: fn(SyntaxKind) -> bool,
message: &'static str,
) {
if self.current_kind().is_none_or(is_at_rule_prelude_boundary) {
return;
}
let valid_name = self
.non_trivia_token_from(self.position)
.is_some_and(|(_, kind)| valid_head(kind));
if !valid_name {
self.error_at_current(ParseErrorCode::ExpectedValue, message);
}
self.consume_at_rule_prelude_tokens();
}
fn parse_import_layer_tail_node(&mut self) {
let valid = self.import_layer_tail_is_valid();
if !valid {
self.error_at_current(ParseErrorCode::ExpectedValue, "invalid @import layer tail");
}
self.builder.start_node(if valid {
SyntaxKind::LayerName
} else {
SyntaxKind::BogusLayerName
});
self.token_current();
if self.current_kind() == Some(SyntaxKind::LeftParen) {
self.parse_balanced_parenthesized_prelude(None);
}
self.builder.finish_node();
}
fn import_layer_tail_is_valid(&self) -> bool {
let Some((open_index, next_kind)) = self.non_trivia_token_from(self.position + 1) else {
return true;
};
if next_kind != SyntaxKind::LeftParen {
return true;
}
let Some(close_index) = self.parenthesized_prelude_close_index(open_index) else {
return false;
};
self.layer_name_is_valid_between(open_index + 1, close_index)
}
fn layer_name_is_valid_between(&self, start: usize, end: usize) -> bool {
let mut saw_name = false;
let mut expecting_segment = true;
for token in self.tokens[start..end]
.iter()
.filter(|token| !token.kind.is_trivia())
{
if is_interpolation_start(token.kind) {
return true;
}
match token.kind {
SyntaxKind::Ident if expecting_segment => {
saw_name = true;
expecting_segment = false;
}
SyntaxKind::Dot if saw_name && !expecting_segment => {
expecting_segment = true;
}
_ => return false,
}
}
saw_name && !expecting_segment
}
fn parse_import_supports_tail_node(&mut self) {
let valid = self.import_supports_tail_is_valid();
if !valid {
self.error_at_current(
ParseErrorCode::ExpectedValue,
"invalid @import supports tail",
);
}
self.builder.start_node(if valid {
SyntaxKind::SupportsCondition
} else {
SyntaxKind::BogusSupportsCondition
});
self.token_current();
if self.current_kind() == Some(SyntaxKind::LeftParen) {
self.parse_balanced_parenthesized_prelude(None);
}
self.builder.finish_node();
}
fn import_supports_tail_is_valid(&self) -> bool {
let Some((open_index, SyntaxKind::LeftParen)) =
self.non_trivia_token_from(self.position + 1)
else {
return false;
};
let Some(close_index) = self.parenthesized_prelude_close_index(open_index) else {
return false;
};
self.non_trivia_token_from(open_index + 1)
.is_some_and(|(inner_index, inner_kind)| {
inner_index < close_index && inner_kind != SyntaxKind::RightParen
})
}
fn consume_at_rule_prelude_tokens(&mut self) {
if self.current_kind().is_none_or(is_at_rule_prelude_boundary) {
return;
}
self.builder
.start_node(self.current_generic_at_rule_prelude_node_kind());
self.consume_at_rule_prelude_tokens_without_wrapping();
self.builder.finish_node();
}
fn consume_at_rule_prelude_tokens_without_wrapping(&mut self) {
while !self.at_end() {
match self.current_kind() {
Some(kind) if is_at_rule_prelude_boundary(kind) => break,
Some(SyntaxKind::LeftParen) => self.parse_balanced_parenthesized_prelude(None),
Some(kind) if is_interpolation_start(kind) => {
self.parse_interpolation(kind, &[SyntaxKind::LeftBrace, SyntaxKind::Semicolon])
}
Some(_) => self.token_current(),
None => break,
}
}
}
fn parse_balanced_parenthesized_prelude(&mut self, node_kind: Option<SyntaxKind>) {
self.parse_balanced_parenthesized_prelude_until(
node_kind,
&[SyntaxKind::LeftBrace, SyntaxKind::Semicolon],
);
}
fn parse_balanced_parenthesized_prelude_until(
&mut self,
node_kind: Option<SyntaxKind>,
recovery: &[SyntaxKind],
) {
if let Some(kind) = node_kind {
self.builder.start_node(kind);
}
let mut depth = 0usize;
let mut closed = false;
while !self.at_end() {
match self.current_kind() {
Some(SyntaxKind::LeftParen) => {
depth += 1;
self.token_current();
}
Some(SyntaxKind::RightParen) => {
self.token_current();
depth = depth.saturating_sub(1);
if depth == 0 {
closed = true;
break;
}
}
Some(kind) if recovery.contains(&kind) => break,
Some(kind) if is_interpolation_start(kind) => {
self.parse_interpolation(kind, &[SyntaxKind::LeftBrace, SyntaxKind::Semicolon])
}
Some(_) => self.token_current(),
None => break,
}
}
if node_kind.is_some() {
self.builder.finish_node();
}
if !closed {
self.error_at_current(
ParseErrorCode::UnexpectedCharacter,
"unterminated parenthesized prelude",
);
}
}
fn parse_interpolation(&mut self, start_kind: SyntaxKind, recovery: &[SyntaxKind]) {
let Some(end_kind) = interpolation_end_kind(start_kind) else {
self.token_current();
return;
};
let closed = self.find_before_recovery(end_kind, recovery);
self.builder.start_node(if closed {
SyntaxKind::Interpolation
} else {
SyntaxKind::BogusInterpolation
});
if self.current_kind() == Some(start_kind) {
self.token_current();
}
while !self.at_end() {
match self.current_kind() {
Some(kind) if kind == end_kind => {
self.token_current();
break;
}
Some(kind) if !closed && recovery.contains(&kind) => break,
Some(_) => self.token_current(),
None => break,
}
}
if !closed {
self.error_at_current(
ParseErrorCode::UnexpectedCharacter,
"unterminated interpolation",
);
}
self.builder.finish_node();
}
fn parse_group_at_rule_block(&mut self) {
self.token_current();
self.builder.start_node(SyntaxKind::RuleList);
self.parse_rule_list_items();
self.builder.finish_node();
if self.current_kind() == Some(SyntaxKind::RightBrace) {
self.token_current();
}
}
fn parse_rule_list_items(&mut self) {
while !self.at_end() {
self.eat_trivia();
match self.current_kind() {
Some(SyntaxKind::RightBrace | SyntaxKind::SassDedent) | None => break,
Some(SyntaxKind::Semicolon | SyntaxKind::SassOptionalSemicolon) => {
self.token_current()
}
Some(SyntaxKind::AtKeyword) if self.current_is_css_module_value_rule() => {
self.parse_css_module_value_rule()
}
Some(SyntaxKind::AtKeyword) if self.current_dialect_at_rule_spec().is_some() => {
self.parse_dialect_at_rule()
}
Some(SyntaxKind::AtKeyword) => self.parse_at_rule(),
Some(_) => self.parse_rule(),
}
}
}
fn parse_declaration_block(&mut self) {
self.token_current();
self.builder
.start_node(if self.previous_left_brace_has_match() {
SyntaxKind::DeclarationList
} else {
SyntaxKind::BogusDeclarationList
});
self.parse_declaration_list();
self.builder.finish_node();
if self.current_kind() == Some(SyntaxKind::RightBrace) {
self.token_current();
} else {
self.missing_token_bogus_trivia(
ParseErrorCode::UnexpectedCharacter,
"unterminated declaration block",
);
}
}
fn parse_sass_indented_at_rule_block(&mut self, block_kind: AtRuleBlockKind) {
self.builder.start_node(SyntaxKind::SassIndentedBlock);
if self.current_kind() == Some(SyntaxKind::SassIndent) {
self.token_current();
}
match block_kind {
AtRuleBlockKind::GroupRuleList => {
self.builder.start_node(SyntaxKind::RuleList);
self.parse_rule_list_items();
self.builder.finish_node();
}
AtRuleBlockKind::DeclarationList | AtRuleBlockKind::Keyframes => {
self.builder.start_node(SyntaxKind::DeclarationList);
self.parse_declaration_list();
self.builder.finish_node();
}
AtRuleBlockKind::Raw => self.consume_sass_indented_raw_body(),
}
if self.current_kind() == Some(SyntaxKind::SassDedent) {
self.token_current();
} else {
self.error_at_current(
ParseErrorCode::UnexpectedCharacter,
"unterminated Sass indented at-rule block",
);
}
self.builder.finish_node();
}
fn consume_sass_indented_raw_body(&mut self) {
let mut depth = 0usize;
while !self.at_end() {
match self.current_kind() {
Some(SyntaxKind::SassIndent) => {
depth += 1;
self.token_current();
}
Some(SyntaxKind::SassDedent) if depth == 0 => break,
Some(SyntaxKind::SassDedent) => {
depth = depth.saturating_sub(1);
self.token_current();
}
Some(_) => self.token_current(),
None => break,
}
}
}
fn parse_keyframes_block(&mut self) {
self.token_current();
while !self.at_end() {
self.eat_trivia();
match self.current_kind() {
Some(SyntaxKind::RightBrace) | None => break,
Some(_) => self.parse_keyframe_block(),
}
}
if self.current_kind() == Some(SyntaxKind::RightBrace) {
self.token_current();
}
}
fn parse_keyframe_block(&mut self) {
let has_block = self.find_before_recovery(SyntaxKind::LeftBrace, &[SyntaxKind::RightBrace]);
self.builder.start_node(if has_block {
SyntaxKind::KeyframeBlock
} else {
SyntaxKind::BogusKeyframeBlock
});
if has_block && !self.keyframe_selector_list_is_valid() {
self.error_at_current(ParseErrorCode::ExpectedValue, "invalid keyframe selector");
}
while !self.at_end() {
match self.current_kind() {
Some(SyntaxKind::LeftBrace) => {
self.parse_declaration_block();
break;
}
Some(SyntaxKind::RightBrace) | None => break,
Some(_) => self.token_current(),
}
}
if !has_block {
self.error_at_current(
ParseErrorCode::UnexpectedCharacter,
"expected keyframe declaration block",
);
}
self.builder.finish_node();
}
fn keyframe_selector_list_is_valid(&self) -> bool {
let mut index = self.position;
let mut saw_selector = false;
let mut expect_selector = true;
loop {
let Some((token_index, kind)) = self.non_trivia_token_from(index) else {
return false;
};
if kind == SyntaxKind::LeftBrace {
return saw_selector && !expect_selector;
}
if expect_selector {
if is_interpolation_start(kind) {
return true;
}
if !keyframe_selector_token_is_valid(self.tokens[token_index]) {
return false;
}
saw_selector = true;
expect_selector = false;
index = token_index + 1;
continue;
}
if kind != SyntaxKind::Comma {
return false;
}
expect_selector = true;
index = token_index + 1;
}
}
fn consume_balanced_block(&mut self) {
let mut depth = 0usize;
while !self.at_end() {
match self.current_kind() {
Some(SyntaxKind::LeftBrace) => {
depth += 1;
self.token_current();
}
Some(SyntaxKind::RightBrace) => {
self.token_current();
depth = depth.saturating_sub(1);
if depth == 0 {
break;
}
}
Some(_) => self.token_current(),
None => break,
}
}
}
fn eat_trivia(&mut self) {
while matches!(self.current_kind(), Some(kind) if kind.is_trivia()) {
self.token_current();
}
}
fn consume_until_recovery(&mut self, recovery: &[SyntaxKind]) {
let should_wrap = self
.current_kind()
.is_some_and(|kind| !recovery.contains(&kind));
if should_wrap {
self.builder.start_node(SyntaxKind::BogusRecovery);
}
while !self.at_end() {
match self.current_kind() {
Some(kind) if recovery.contains(&kind) => break,
Some(_) => self.token_current(),
None => break,
}
}
if should_wrap {
self.builder.finish_node();
}
}
fn find_before_recovery(&self, target: SyntaxKind, recovery: &[SyntaxKind]) -> bool {
let mut index = self.position;
while let Some(token) = self.tokens.get(index) {
if token.kind == target {
return true;
}
if recovery.contains(&token.kind) {
return false;
}
index += 1;
}
false
}
fn find_rule_block_open_before_recovery(&self, recovery: &[SyntaxKind]) -> bool {
let mut index = self.position;
while let Some(token) = self.tokens.get(index) {
if token.kind == SyntaxKind::LeftBrace
|| (self.dialect == StyleDialect::Sass && token.kind == SyntaxKind::SassIndent)
{
return true;
}
if recovery.contains(&token.kind) {
return false;
}
index += 1;
}
false
}
fn find_text_before_recovery(&self, target: &str, recovery: &[SyntaxKind]) -> bool {
let mut index = self.position;
while let Some(token) = self.tokens.get(index) {
if token.text == target {
return true;
}
if recovery.contains(&token.kind) {
return false;
}
index += 1;
}
false
}
fn current_function_has_closing_paren_before(&self, recovery: &[SyntaxKind]) -> bool {
let Some(open_index) = self.position.checked_add(1) else {
return false;
};
if self
.tokens
.get(open_index)
.is_none_or(|token| token.kind != SyntaxKind::LeftParen)
{
return false;
}
let mut depth = 0usize;
for token in self.tokens.iter().skip(open_index) {
match token.kind {
SyntaxKind::LeftParen => depth += 1,
SyntaxKind::RightParen => {
depth = depth.saturating_sub(1);
if depth == 0 {
return true;
}
}
kind if depth == 1 && recovery.contains(&kind) => return false,
_ => {}
}
}
false
}
fn current_split_important_annotation(&self) -> bool {
self.current_text() == Some("!")
&& self
.non_trivia_token_from(self.position + 1)
.is_some_and(|(index, kind)| {
matches!(kind, SyntaxKind::Ident | SyntaxKind::KeywordImportant)
&& self
.tokens
.get(index)
.is_some_and(|token| token.text.eq_ignore_ascii_case("important"))
})
}
fn current_scss_variable_flag_annotation(&self) -> bool {
matches!(self.dialect, StyleDialect::Scss | StyleDialect::Sass)
&& self.current_text() == Some("!")
&& self
.non_trivia_token_from(self.position + 1)
.is_some_and(|(index, kind)| {
kind == SyntaxKind::Ident
&& self.tokens.get(index).is_some_and(|token| {
token.text.eq_ignore_ascii_case("default")
|| token.text.eq_ignore_ascii_case("global")
})
})
}
fn current_bracketed_value_has_closing_bracket_before(&self, recovery: &[SyntaxKind]) -> bool {
let mut depth = 0usize;
for token in self.tokens.iter().skip(self.position) {
match token.kind {
SyntaxKind::LeftBracket => depth += 1,
SyntaxKind::RightBracket => {
depth = depth.saturating_sub(1);
if depth == 0 {
return true;
}
}
kind if depth == 1 && recovery.contains(&kind) => return false,
_ => {}
}
}
false
}
fn current_simple_block_has_matching_close(&self, recovery: &[SyntaxKind]) -> bool {
let Some(open_kind) = self.current_kind() else {
return false;
};
if matching_simple_block_close(open_kind).is_none() {
return false;
}
let mut expected_closes = Vec::new();
for token in self.tokens.iter().skip(self.position) {
if let Some(close_kind) = matching_simple_block_close(token.kind) {
expected_closes.push(close_kind);
continue;
}
if expected_closes.last().copied() == Some(token.kind) {
expected_closes.pop();
if expected_closes.is_empty() {
return true;
}
continue;
}
if expected_closes.len() == 1 && recovery.contains(&token.kind) {
return false;
}
}
false
}
fn current_dialect_at_rule_node_kind(&self, spec: AtRuleSpec) -> SyntaxKind {
if !self.find_rule_block_open_before_recovery(&[
SyntaxKind::Semicolon,
SyntaxKind::SassOptionalSemicolon,
SyntaxKind::RightBrace,
SyntaxKind::SassDedent,
]) {
return match spec.node_kind {
SyntaxKind::ScssMixinDeclaration => SyntaxKind::BogusScssMixin,
SyntaxKind::ScssFunctionDeclaration => SyntaxKind::BogusScssFunction,
SyntaxKind::ScssControlIf
| SyntaxKind::ScssControlElse
| SyntaxKind::ScssControlEach
| SyntaxKind::ScssControlFor
| SyntaxKind::ScssControlWhile => SyntaxKind::BogusScssControl,
_ => spec.node_kind,
};
}
spec.node_kind
}
fn current_less_guard_has_condition_before(&self, recovery: &[SyntaxKind]) -> bool {
let mut index = self.position + 1;
while let Some(token) = self.tokens.get(index) {
if recovery.contains(&token.kind) {
return false;
}
if token.kind == SyntaxKind::LeftParen {
return true;
}
index += 1;
}
false
}
fn current_scss_module_config_has_balanced_parens(&self) -> bool {
let Some((_, SyntaxKind::LeftParen)) = self.non_trivia_token_from(self.position + 1) else {
return false;
};
self.current_prelude_parentheses_are_balanced_until(&[
SyntaxKind::Semicolon,
SyntaxKind::SassOptionalSemicolon,
SyntaxKind::LeftBrace,
SyntaxKind::SassIndent,
])
}
fn current_value_has_top_level_comma_before(&self, recovery: &[SyntaxKind]) -> bool {
let mut paren_depth = 0usize;
let mut bracket_depth = 0usize;
for token in self.tokens.iter().skip(self.position) {
match token.kind {
kind if paren_depth == 0 && bracket_depth == 0 && recovery.contains(&kind) => {
return false;
}
SyntaxKind::LeftParen => paren_depth += 1,
SyntaxKind::RightParen => paren_depth = paren_depth.saturating_sub(1),
SyntaxKind::LeftBracket => bracket_depth += 1,
SyntaxKind::RightBracket => bracket_depth = bracket_depth.saturating_sub(1),
SyntaxKind::Comma if paren_depth == 0 && bracket_depth == 0 => return true,
_ => {}
}
}
false
}
fn current_value_list_is_bogus(&self, recovery: &[SyntaxKind]) -> bool {
let mut paren_depth = 0usize;
let mut bracket_depth = 0usize;
let mut expecting_item = true;
for token in self.tokens.iter().skip(self.position) {
if token.kind.is_trivia() {
continue;
}
match token.kind {
kind if paren_depth == 0 && bracket_depth == 0 && recovery.contains(&kind) => {
return expecting_item;
}
SyntaxKind::LeftParen => {
paren_depth += 1;
expecting_item = false;
}
SyntaxKind::RightParen => {
paren_depth = paren_depth.saturating_sub(1);
expecting_item = false;
}
SyntaxKind::LeftBracket => {
bracket_depth += 1;
expecting_item = false;
}
SyntaxKind::RightBracket => {
bracket_depth = bracket_depth.saturating_sub(1);
expecting_item = false;
}
SyntaxKind::Comma if paren_depth == 0 && bracket_depth == 0 => {
if expecting_item {
return true;
}
expecting_item = true;
}
_ => expecting_item = false,
}
}
expecting_item
}
fn current_starts_missing_semicolon_declaration(&self, recovery: &[SyntaxKind]) -> bool {
match self.current_kind() {
Some(SyntaxKind::Ident | SyntaxKind::CustomPropertyName) => {}
_ => return false,
}
let mut index = self.position + 1;
while let Some(token) = self.tokens.get(index) {
if token.kind.is_trivia() {
index += 1;
continue;
}
if recovery.contains(&token.kind) {
return false;
}
return token.kind == SyntaxKind::Colon;
}
false
}
fn current_selector_item_is_bogus(&self, recovery: &[SyntaxKind]) -> bool {
self.selector_item_is_bogus_from(self.position, recovery)
}
fn selector_item_is_bogus_from(&self, start: usize, recovery: &[SyntaxKind]) -> bool {
let mut paren_depth = 0usize;
let mut bracket_depth = 0usize;
let mut saw_selector_token = false;
for token in self.tokens.iter().skip(start) {
if token.kind.is_trivia() {
continue;
}
if paren_depth == 0
&& bracket_depth == 0
&& (token.kind == SyntaxKind::Comma
|| is_selector_boundary_until(token.kind, recovery))
{
break;
}
match token.kind {
SyntaxKind::LeftParen => paren_depth += 1,
SyntaxKind::RightParen => paren_depth = paren_depth.saturating_sub(1),
SyntaxKind::LeftBracket => bracket_depth += 1,
SyntaxKind::RightBracket => bracket_depth = bracket_depth.saturating_sub(1),
_ => {}
}
if !selector_item_token_is_recoverable(token.kind) {
return true;
}
saw_selector_token = true;
}
!saw_selector_token
}
fn selector_list_contains_bogus_item_until(&self, recovery: &[SyntaxKind]) -> bool {
let mut index = self.position;
while let Some(token) = self.tokens.get(index) {
if token.kind.is_trivia() || token.kind == SyntaxKind::Comma {
index += 1;
continue;
}
if is_selector_boundary_until(token.kind, recovery) {
return false;
}
if self.selector_item_is_bogus_from(index, recovery) {
return true;
}
let mut paren_depth = 0usize;
let mut bracket_depth = 0usize;
while let Some(token) = self.tokens.get(index) {
if paren_depth == 0
&& bracket_depth == 0
&& (token.kind == SyntaxKind::Comma
|| is_selector_boundary_until(token.kind, recovery))
{
break;
}
match token.kind {
SyntaxKind::LeftParen => paren_depth += 1,
SyntaxKind::RightParen => paren_depth = paren_depth.saturating_sub(1),
SyntaxKind::LeftBracket => bracket_depth += 1,
SyntaxKind::RightBracket => bracket_depth = bracket_depth.saturating_sub(1),
_ => {}
}
index += 1;
}
}
false
}
fn current_generic_at_rule_prelude_node_kind(&self) -> SyntaxKind {
if self.current_prelude_parentheses_are_balanced_until(&[
SyntaxKind::LeftBrace,
SyntaxKind::Semicolon,
]) {
SyntaxKind::AtRulePrelude
} else {
SyntaxKind::BogusAtRulePrelude
}
}
fn current_prelude_parentheses_are_balanced_until(&self, recovery: &[SyntaxKind]) -> bool {
let mut depth = 0usize;
for token in self.tokens.iter().skip(self.position) {
match token.kind {
kind if depth == 0 && recovery.contains(&kind) => return true,
SyntaxKind::LeftParen => depth += 1,
SyntaxKind::RightParen => {
if depth == 0 {
return false;
}
depth -= 1;
}
_ => {}
}
}
depth == 0
}
fn previous_left_brace_has_match(&self) -> bool {
let Some(open_index) = self.position.checked_sub(1) else {
return false;
};
let Some(open) = self.tokens.get(open_index) else {
return false;
};
if open.kind != SyntaxKind::LeftBrace {
return false;
}
let mut depth = 0usize;
for token in self.tokens.iter().skip(open_index) {
match token.kind {
SyntaxKind::LeftBrace => depth += 1,
SyntaxKind::RightBrace => {
depth = depth.saturating_sub(1);
if depth == 0 {
return true;
}
}
_ => {}
}
}
false
}
fn current_starts_nested_rule(&self) -> bool {
matches!(
self.current_kind(),
Some(
SyntaxKind::Dot
| SyntaxKind::Hash
| SyntaxKind::Ampersand
| SyntaxKind::Colon
| SyntaxKind::DoubleColon
| SyntaxKind::LeftBracket
)
) && self.find_rule_block_open_before_recovery(&[
SyntaxKind::Colon,
SyntaxKind::Semicolon,
SyntaxKind::SassOptionalSemicolon,
SyntaxKind::RightBrace,
SyntaxKind::SassDedent,
])
}
fn current_starts_scss_nested_property(&self) -> bool {
if !matches!(self.dialect, StyleDialect::Scss | StyleDialect::Sass) {
return false;
}
if !matches!(
self.current_kind(),
Some(SyntaxKind::Ident | SyntaxKind::CustomPropertyName)
) {
return false;
}
let mut saw_colon = false;
for token in self.tokens.iter().skip(self.position) {
match token.kind {
SyntaxKind::Colon => saw_colon = true,
SyntaxKind::LeftBrace if saw_colon => return true,
SyntaxKind::SassIndent if saw_colon && self.dialect == StyleDialect::Sass => {
return true;
}
SyntaxKind::Semicolon
| SyntaxKind::SassOptionalSemicolon
| SyntaxKind::RightBrace
| SyntaxKind::SassDedent => return false,
_ => {}
}
}
false
}
fn current_starts_less_mixin_declaration(&self) -> bool {
self.dialect == StyleDialect::Less
&& self.current_starts_less_callable_signature()
&& self.find_before_recovery(
SyntaxKind::LeftBrace,
&[SyntaxKind::Semicolon, SyntaxKind::RightBrace],
)
}
fn current_starts_less_mixin_call(&self) -> bool {
self.dialect == StyleDialect::Less
&& self.current_starts_less_callable_signature()
&& !self.find_before_recovery(
SyntaxKind::LeftBrace,
&[SyntaxKind::Semicolon, SyntaxKind::RightBrace],
)
}
fn current_starts_less_callable_signature(&self) -> bool {
match self.current_kind() {
Some(SyntaxKind::Dot) => {
let Some((index, SyntaxKind::Ident | SyntaxKind::CustomPropertyName)) =
self.non_trivia_token_from(self.position + 1)
else {
return false;
};
self.non_trivia_token_from(index + 1)
.is_some_and(|(_, kind)| kind == SyntaxKind::LeftParen)
}
Some(SyntaxKind::Hash) => self
.non_trivia_token_from(self.position + 1)
.is_some_and(|(_, kind)| kind == SyntaxKind::LeftParen),
_ => false,
}
}
fn current_starts_less_extend_rule(&self) -> bool {
self.dialect == StyleDialect::Less
&& self.current_kind() == Some(SyntaxKind::Colon)
&& self
.non_trivia_token_from(self.position + 1)
.is_some_and(|(index, kind)| {
kind == SyntaxKind::Ident
&& self
.tokens
.get(index)
.is_some_and(|token| token.text == "extend")
})
}
fn current_starts_less_namespace_access(&self) -> bool {
self.dialect == StyleDialect::Less
&& matches!(
self.current_kind(),
Some(SyntaxKind::Dot | SyntaxKind::Hash)
)
&& self.find_before_recovery(
SyntaxKind::GreaterThan,
&[
SyntaxKind::Semicolon,
SyntaxKind::LeftBrace,
SyntaxKind::RightBrace,
],
)
&& self.find_before_recovery(
SyntaxKind::LeftParen,
&[
SyntaxKind::Semicolon,
SyntaxKind::LeftBrace,
SyntaxKind::RightBrace,
],
)
}
fn current_left_brace_has_match(&self) -> bool {
let mut depth = 0usize;
for token in self.tokens.iter().skip(self.position) {
match token.kind {
SyntaxKind::LeftBrace => depth += 1,
SyntaxKind::RightBrace => {
depth = depth.saturating_sub(1);
if depth == 0 {
return true;
}
}
_ => {}
}
}
false
}
fn token_current(&mut self) {
if let Some(token) = self.tokens.get(self.position).copied() {
self.builder.token(token.kind, token.text);
self.position += 1;
}
}
fn empty_bogus_node(&mut self, kind: SyntaxKind, code: ParseErrorCode, message: &'static str) {
self.builder.start_node(kind);
self.builder.finish_node();
self.error_at_current(code, message);
}
fn missing_token_bogus_trivia(&mut self, code: ParseErrorCode, message: &'static str) {
self.builder.start_node(SyntaxKind::BogusTrivia);
self.builder.finish_node();
self.error_at_current(code, message);
}
fn error_at_current(&mut self, code: ParseErrorCode, message: &'static str) {
self.errors.push(ParseError {
code,
range: self.current_range(),
message,
});
}
fn current_kind(&self) -> Option<SyntaxKind> {
self.tokens.get(self.position).map(|token| token.kind)
}
fn current_range(&self) -> TextRange {
if let Some(token) = self.tokens.get(self.position) {
return token.range;
}
let end = self
.tokens
.last()
.map(|token| token.range.end())
.unwrap_or_else(|| TextSize::from(0));
TextRange::new(end, end)
}
fn current_text(&self) -> Option<&'text str> {
self.tokens.get(self.position).map(|token| token.text)
}
fn current_dialect_at_rule_spec(&self) -> Option<AtRuleSpec> {
let text = self.current_text()?;
match self.dialect {
StyleDialect::Scss | StyleDialect::Sass => scss_at_rule_spec(text),
StyleDialect::Css | StyleDialect::Less => None,
}
}
fn current_is_css_module_value_rule(&self) -> bool {
self.current_text() == Some("@value")
}
fn next_kind(&self) -> Option<SyntaxKind> {
self.tokens.get(self.position + 1).map(|token| token.kind)
}
fn next_non_trivia_kind(&self) -> Option<SyntaxKind> {
let mut index = self.position + 1;
while let Some(token) = self.tokens.get(index) {
if !token.kind.is_trivia() {
return Some(token.kind);
}
index += 1;
}
None
}
fn non_trivia_token_from(&self, mut index: usize) -> Option<(usize, SyntaxKind)> {
while let Some(token) = self.tokens.get(index) {
if !token.kind.is_trivia() {
return Some((index, token.kind));
}
index += 1;
}
None
}
fn non_trivia_token_after_interpolation(
&self,
mut index: usize,
start_kind: SyntaxKind,
) -> Option<(usize, SyntaxKind)> {
let end_kind = interpolation_end_kind(start_kind)?;
index += 1;
while let Some(token) = self.tokens.get(index) {
if token.kind == end_kind {
return self.non_trivia_token_from(index + 1);
}
if is_at_rule_prelude_boundary(token.kind) {
return None;
}
index += 1;
}
None
}
fn current_starts_namespace_qualified_selector(&self, kind: SyntaxKind) -> bool {
match kind {
SyntaxKind::Ident | SyntaxKind::Star => {
self.next_kind() == Some(SyntaxKind::Pipe)
&& self
.tokens
.get(self.position + 2)
.is_some_and(|token| namespace_selector_target_can_start(token.kind))
}
SyntaxKind::Pipe => self
.tokens
.get(self.position + 1)
.is_some_and(|token| namespace_selector_target_can_start(token.kind)),
_ => false,
}
}
fn namespace_qualified_selector_target_kind(&self) -> Option<SyntaxKind> {
let target_index = if self.current_kind() == Some(SyntaxKind::Pipe) {
self.position + 1
} else {
self.position + 2
};
self.tokens.get(target_index).map(|token| token.kind)
}
fn at_end(&self) -> bool {
self.position >= self.tokens.len()
}
}
impl<'text, 'extension, E> Tokenizer<'text, 'extension, E>
where
E: DialectExtension,
{
fn new(text: &'text str, extension: &'extension E) -> Self {
Self {
text,
extension,
offset: 0,
scss_interpolation_depth: 0,
less_interpolation_depth: 0,
sass_indent_stack: vec![0],
tokens: Vec::new(),
errors: Vec::new(),
}
}
fn tokenize(&mut self) {
while let Some(current) = self.current_char() {
let start = self.offset;
match current {
'\u{feff}' if start == 0 => self.bump_current(),
'\r' | '\n' if self.extension.dialect() == StyleDialect::Sass => {
self.consume_sass_indented_newline(start)
}
char if char.is_whitespace() => {
self.consume_while(SyntaxKind::Whitespace, |c| c.is_whitespace())
}
'/' if self.starts_with("/*") => self.consume_block_comment(),
'/' if self.starts_with("//") && self.extension.dialect() != StyleDialect::Css => {
self.consume_line_comment()
}
'#' if self.starts_with("#{") && self.supports_scss_interpolation() => {
self.consume_scss_interpolation_start(start)
}
'@' if self.starts_with("@{") && self.supports_less_interpolation() => {
self.consume_less_interpolation_start(start)
}
'!' if self.starts_with_ascii_keyword("!important") => {
self.consume_static(SyntaxKind::Important, start, "!important".len())
}
'<' if self.starts_with("<!--") => {
self.consume_static(SyntaxKind::Cdo, start, "<!--".len())
}
'-' if self.starts_with("-->") => {
self.consume_static(SyntaxKind::Cdc, start, "-->".len())
}
'"' | '\'' => self.consume_string(current),
'u' | 'U' if self.starts_unicode_range() => self.consume_unicode_range(),
'0'..='9' => self.consume_number(),
'$' if matches!(
self.extension.dialect(),
StyleDialect::Scss | StyleDialect::Sass
) =>
{
self.consume_prefixed_name(SyntaxKind::ScssVariable)
}
'@' if self.extension.dialect() == StyleDialect::Less => {
self.consume_less_at_name()
}
'@' => self.consume_at_keyword(),
'!' => self.consume_static(SyntaxKind::Delim, start, 1),
'.' if self.current_starts_number() => self.consume_number(),
'.' => self.consume_static(SyntaxKind::Dot, start, 1),
',' => self.consume_static(SyntaxKind::Comma, start, 1),
':' if self.starts_with("::") => {
self.consume_static(SyntaxKind::DoubleColon, start, 2)
}
':' => self.consume_static(SyntaxKind::Colon, start, 1),
';' => self.consume_static(SyntaxKind::Semicolon, start, 1),
'{' => self.consume_static(SyntaxKind::LeftBrace, start, 1),
'}' if self.scss_interpolation_depth > 0 => {
self.consume_scss_interpolation_end(start)
}
'}' if self.less_interpolation_depth > 0 => {
self.consume_less_interpolation_end(start)
}
'}' => self.consume_static(SyntaxKind::RightBrace, start, 1),
'(' => self.consume_static(SyntaxKind::LeftParen, start, 1),
')' => self.consume_static(SyntaxKind::RightParen, start, 1),
'[' => self.consume_static(SyntaxKind::LeftBracket, start, 1),
']' => self.consume_static(SyntaxKind::RightBracket, start, 1),
'+' if self.starts_with("+=") => {
self.consume_static(SyntaxKind::PlusEquals, start, 2)
}
'+' if self.current_starts_number() => self.consume_number(),
'+' => self.consume_static(SyntaxKind::Plus, start, 1),
'-' if self.starts_with("-=") => {
self.consume_static(SyntaxKind::MinusEquals, start, 2)
}
'-' if self.current_starts_number() => self.consume_number(),
'-' if self.current_starts_ident_sequence() => self.consume_ident_like(),
'-' => self.consume_static(SyntaxKind::Minus, start, 1),
'*' if self.starts_with("*=") => {
self.consume_static(SyntaxKind::SubstringMatch, start, 2)
}
'*' => self.consume_static(SyntaxKind::Star, start, 1),
'/' if self.starts_with("/=") => {
self.consume_static(SyntaxKind::SlashEquals, start, 2)
}
'/' => self.consume_static(SyntaxKind::Slash, start, 1),
'%' if self.starts_scss_placeholder() => {
self.consume_prefixed_name(SyntaxKind::ScssPlaceholder)
}
'%' => self.consume_static(SyntaxKind::Percent, start, 1),
'=' if self.starts_with("=>") => self.consume_static(SyntaxKind::Arrow, start, 2),
'=' => self.consume_static(SyntaxKind::Equals, start, 1),
'~' if self.starts_less_escaped_string() => self.consume_less_escaped_string(start),
'~' if self.starts_with("~=") => {
self.consume_static(SyntaxKind::IncludesMatch, start, 2)
}
'~' => self.consume_static(SyntaxKind::Tilde, start, 1),
'|' if self.starts_with("|=") => {
self.consume_static(SyntaxKind::DashMatch, start, 2)
}
'|' if self.starts_with("||") => {
self.consume_static(SyntaxKind::ColumnCombinator, start, 2)
}
'|' => self.consume_static(SyntaxKind::Pipe, start, 1),
'^' if self.starts_with("^=") => {
self.consume_static(SyntaxKind::PrefixMatch, start, 2)
}
'^' => self.consume_static(SyntaxKind::Caret, start, 1),
'$' if self.starts_with("$=") => {
self.consume_static(SyntaxKind::SuffixMatch, start, 2)
}
'$' if self.starts_less_property_variable() => {
self.consume_prefixed_name(SyntaxKind::LessPropertyVariableToken)
}
'&' if self.starts_with("&&") => {
self.consume_static(SyntaxKind::DoubleAmpersand, start, 2)
}
'&' => self.consume_static(SyntaxKind::Ampersand, start, 1),
'>' => self.consume_static(SyntaxKind::GreaterThan, start, 1),
'<' => self.consume_static(SyntaxKind::LessThan, start, 1),
'#' if self.current_hash_starts_name() => self.consume_name_like(SyntaxKind::Hash),
'#' => self.consume_static(SyntaxKind::Delim, start, 1),
'\\' if self.current_starts_valid_escape() => {
self.consume_name_like(SyntaxKind::Ident)
}
char if is_name_start(char) => self.consume_ident_like(),
char => self.consume_unexpected(char),
}
}
self.consume_pending_sass_dedents();
}
fn consume_static(&mut self, kind: SyntaxKind, start: usize, byte_len: usize) {
self.offset += byte_len;
self.push(kind, start, self.offset);
}
fn consume_while(&mut self, kind: SyntaxKind, predicate: impl Fn(char) -> bool) {
let start = self.offset;
while let Some(char) = self.current_char() {
if !predicate(char) {
break;
}
self.bump_char(char);
}
self.push(kind, start, self.offset);
}
fn consume_block_comment(&mut self) {
let start = self.offset;
self.offset += 2;
while self.offset < self.text.len() {
if self.starts_with("*/") {
self.offset += 2;
self.push(SyntaxKind::BlockComment, start, self.offset);
return;
}
match self.current_char() {
Some(char) => self.bump_char(char),
None => break,
}
}
self.push(SyntaxKind::BlockComment, start, self.offset);
self.error(
ParseErrorCode::UnterminatedBlockComment,
start,
self.offset,
"unterminated block comment",
);
}
fn consume_line_comment(&mut self) {
let start = self.offset;
while let Some(char) = self.current_char() {
if char == '\n' {
break;
}
if char == '\r' {
break;
}
self.bump_char(char);
}
self.push(SyntaxKind::LineComment, start, self.offset);
}
fn consume_sass_indented_newline(&mut self, start: usize) {
self.consume_line_break();
let indent = self.consume_sass_line_indent();
let line_start = self.offset;
let current_indent = self.sass_indent_stack.last().copied().unwrap_or(0);
if indent > current_indent {
self.push(SyntaxKind::SassIndentedNewline, start, line_start);
self.sass_indent_stack.push(indent);
self.push(SyntaxKind::SassIndent, line_start, line_start);
return;
}
if self.previous_significant_sass_token_can_end_statement() {
self.push(SyntaxKind::SassOptionalSemicolon, start, start);
}
self.push(SyntaxKind::SassIndentedNewline, start, line_start);
while self.sass_indent_stack.len() > 1
&& self
.sass_indent_stack
.last()
.is_some_and(|current| indent < *current)
{
self.sass_indent_stack.pop();
self.push(SyntaxKind::SassDedent, line_start, line_start);
}
if self
.sass_indent_stack
.last()
.is_some_and(|current| indent != *current)
{
self.error(
ParseErrorCode::UnexpectedCharacter,
line_start,
line_start,
"inconsistent Sass indentation",
);
}
}
fn consume_line_break(&mut self) {
if self.starts_with("\r\n") {
self.offset += "\r\n".len();
return;
}
if let Some(char @ ('\r' | '\n')) = self.current_char() {
self.bump_char(char);
}
}
fn consume_sass_line_indent(&mut self) -> usize {
let mut indent = 0usize;
while let Some(char) = self.current_char() {
match char {
' ' => {
indent += 1;
self.bump_char(char);
}
'\t' => {
indent += 4;
self.bump_char(char);
}
_ => break,
}
}
indent
}
fn consume_pending_sass_dedents(&mut self) {
if self.extension.dialect() != StyleDialect::Sass {
return;
}
while self.sass_indent_stack.len() > 1 {
self.sass_indent_stack.pop();
self.push(SyntaxKind::SassDedent, self.offset, self.offset);
}
}
fn previous_significant_sass_token_can_end_statement(&self) -> bool {
self.tokens
.iter()
.rev()
.find(|token| !token.kind.is_trivia())
.is_some_and(|token| sass_token_can_end_statement(token.kind))
}
fn consume_scss_interpolation_start(&mut self, start: usize) {
self.offset += "#{".len();
self.scss_interpolation_depth += 1;
self.push(SyntaxKind::ScssInterpolationStart, start, self.offset);
}
fn consume_scss_interpolation_end(&mut self, start: usize) {
self.offset += '}'.len_utf8();
self.scss_interpolation_depth = self.scss_interpolation_depth.saturating_sub(1);
self.push(SyntaxKind::ScssInterpolationEnd, start, self.offset);
}
fn consume_less_interpolation_start(&mut self, start: usize) {
self.offset += "@{".len();
self.less_interpolation_depth += 1;
self.push(SyntaxKind::LessInterpolationStart, start, self.offset);
}
fn consume_less_interpolation_end(&mut self, start: usize) {
self.offset += '}'.len_utf8();
self.less_interpolation_depth = self.less_interpolation_depth.saturating_sub(1);
self.push(SyntaxKind::LessInterpolationEnd, start, self.offset);
}
fn consume_string(&mut self, quote: char) {
let start = self.offset;
self.bump_char(quote);
while let Some(char) = self.current_char() {
self.bump_char(char);
if matches!(char, '\n' | '\r' | '\u{000c}') {
self.push(SyntaxKind::BadString, start, self.offset);
self.error(
ParseErrorCode::UnterminatedString,
start,
self.offset,
"unterminated string",
);
return;
}
if char == quote {
self.push(SyntaxKind::String, start, self.offset);
return;
}
if char == '\\'
&& let Some(escaped) = self.current_char()
{
self.bump_char(escaped);
}
}
self.push(SyntaxKind::BadString, start, self.offset);
self.error(
ParseErrorCode::UnterminatedString,
start,
self.offset,
"unterminated string",
);
}
fn consume_less_escaped_string(&mut self, start: usize) {
self.offset += '~'.len_utf8();
let Some(quote @ ('"' | '\'')) = self.current_char() else {
self.push(SyntaxKind::Tilde, start, self.offset);
return;
};
self.bump_char(quote);
while let Some(char) = self.current_char() {
self.bump_char(char);
if matches!(char, '\n' | '\r' | '\u{000c}') {
self.push(SyntaxKind::BadString, start, self.offset);
self.error(
ParseErrorCode::UnterminatedString,
start,
self.offset,
"unterminated Less escaped string",
);
return;
}
if char == quote {
self.push(SyntaxKind::LessEscapedString, start, self.offset);
return;
}
if char == '\\'
&& let Some(escaped) = self.current_char()
{
self.bump_char(escaped);
}
}
self.push(SyntaxKind::BadString, start, self.offset);
self.error(
ParseErrorCode::UnterminatedString,
start,
self.offset,
"unterminated Less escaped string",
);
}
fn consume_number(&mut self) {
let start = self.offset;
if matches!(self.current_char(), Some('+' | '-')) {
self.bump_current();
}
self.consume_digits();
if self.current_char() == Some('.') && self.char_after_current_is_ascii_digit() {
self.bump_current();
self.consume_digits();
}
if self.current_starts_number_exponent() {
self.bump_current();
if matches!(self.current_char(), Some('+' | '-')) {
self.bump_current();
}
self.consume_digits();
}
if self.current_char() == Some('%') {
self.offset += 1;
self.push(SyntaxKind::Percentage, start, self.offset);
return;
}
if self.current_starts_ident_sequence() {
self.consume_name_continue_sequence();
self.push(SyntaxKind::Dimension, start, self.offset);
return;
}
self.push(SyntaxKind::Number, start, self.offset);
}
fn consume_unicode_range(&mut self) {
let start = self.offset;
self.bump_current();
self.offset += '+'.len_utf8();
self.consume_unicode_range_codepoints(true);
if self.current_char() == Some('-') && self.next_char_is_hex_digit() {
self.bump_current();
self.consume_unicode_range_codepoints(false);
}
self.push(SyntaxKind::UnicodeRange, start, self.offset);
}
fn consume_unicode_range_codepoints(&mut self, allow_question_mark: bool) {
let mut consumed = 0usize;
while consumed < 6 {
match self.current_char() {
Some(char) if char.is_ascii_hexdigit() => {
self.bump_char(char);
consumed += 1;
}
Some('?') if allow_question_mark => {
self.bump_current();
consumed += 1;
}
_ => break,
}
}
}
fn consume_digits(&mut self) {
while matches!(self.current_char(), Some('0'..='9')) {
self.offset += 1;
}
}
fn consume_prefixed_name(&mut self, preferred_kind: SyntaxKind) {
let start = self.offset;
self.bump_current();
while matches!(self.current_char(), Some(char) if is_name_continue(char)) {
self.bump_current();
}
let text = &self.text[start..self.offset];
let kind = self
.extension
.classify_variable_token(text)
.unwrap_or(preferred_kind);
self.push(kind, start, self.offset);
}
fn consume_less_at_name(&mut self) {
let start = self.offset;
self.bump_current();
while matches!(self.current_char(), Some(char) if is_name_continue(char)) {
self.bump_current();
}
let text = &self.text[start..self.offset];
let kind = if is_css_at_rule_name(text) {
SyntaxKind::AtKeyword
} else {
self.extension
.classify_variable_token(text)
.unwrap_or(SyntaxKind::LessVariable)
};
self.push(kind, start, self.offset);
}
fn consume_at_keyword(&mut self) {
let start = self.offset;
self.bump_current();
while matches!(self.current_char(), Some(char) if is_name_continue(char)) {
self.bump_current();
}
self.push(SyntaxKind::AtKeyword, start, self.offset);
}
fn consume_name_like(&mut self, kind: SyntaxKind) {
let start = self.offset;
self.consume_name_start();
self.consume_name_continue_sequence();
self.push(kind, start, self.offset);
}
fn consume_ident_like(&mut self) {
let start = self.offset;
self.consume_name_continue_sequence();
let ident = &self.text[start..self.offset];
if ident.eq_ignore_ascii_case("url")
&& self.current_char() == Some('(')
&& !self.url_starts_with_quoted_argument()
{
self.consume_url_token(start);
return;
}
let kind = if is_custom_property_name_text(ident) {
SyntaxKind::CustomPropertyName
} else {
SyntaxKind::Ident
};
self.push(kind, start, self.offset);
}
fn consume_name_start(&mut self) {
if self.current_starts_valid_escape() {
self.consume_name_escape();
} else {
self.bump_current();
}
}
fn consume_name_continue_sequence(&mut self) {
loop {
if self.current_starts_valid_escape() {
self.consume_name_escape();
} else if matches!(self.current_char(), Some(char) if is_name_continue(char)) {
self.bump_current();
} else {
break;
}
}
}
fn consume_name_escape(&mut self) {
self.bump_current();
let mut hex_digits = 0usize;
while hex_digits < 6
&& matches!(self.current_char(), Some(char) if char.is_ascii_hexdigit())
{
self.bump_current();
hex_digits += 1;
}
if hex_digits > 0 {
if matches!(self.current_char(), Some(char) if char.is_whitespace()) {
self.bump_current();
}
} else if self.current_char().is_some() {
self.bump_current();
}
}
fn consume_url_token(&mut self, start: usize) {
self.bump_current();
while matches!(self.current_char(), Some(char) if char.is_whitespace()) {
self.bump_current();
}
while let Some(char) = self.current_char() {
match char {
')' => {
self.bump_current();
self.push(SyntaxKind::Url, start, self.offset);
return;
}
char if char.is_whitespace() => {
self.bump_current();
while matches!(self.current_char(), Some(char) if char.is_whitespace()) {
self.bump_current();
}
if self.current_char() == Some(')') {
self.bump_current();
self.push(SyntaxKind::Url, start, self.offset);
return;
}
self.consume_bad_url(start);
return;
}
'"' | '\'' | '(' => {
self.consume_bad_url(start);
return;
}
'\\' if self.current_starts_valid_escape() => {
self.consume_name_escape();
}
'\\' => {
self.consume_bad_url(start);
return;
}
char if is_non_printable_code_point(char) => {
self.consume_bad_url(start);
return;
}
_ => self.bump_current(),
}
}
self.push(SyntaxKind::BadUrl, start, self.offset);
self.error(
ParseErrorCode::UnexpectedCharacter,
start,
self.offset,
"unterminated url token",
);
}
fn consume_bad_url(&mut self, start: usize) {
while let Some(char) = self.current_char() {
if char == ')' {
self.bump_current();
break;
}
if self.current_starts_valid_escape() {
self.consume_name_escape();
} else {
self.bump_current();
}
}
self.push(SyntaxKind::BadUrl, start, self.offset);
self.error(
ParseErrorCode::UnexpectedCharacter,
start,
self.offset,
"bad url token",
);
}
fn url_starts_with_quoted_argument(&self) -> bool {
let Some(mut rest) = self.text.get(self.offset + '('.len_utf8()..) else {
return false;
};
rest = rest.trim_start_matches(char::is_whitespace);
matches!(rest.chars().next(), Some('"' | '\''))
}
fn starts_less_property_variable(&self) -> bool {
self.extension.dialect() == StyleDialect::Less
&& self.text[self.offset + '$'.len_utf8()..]
.chars()
.next()
.is_some_and(is_name_start)
}
fn starts_scss_placeholder(&self) -> bool {
matches!(
self.extension.dialect(),
StyleDialect::Scss | StyleDialect::Sass
) && self.text[self.offset + '%'.len_utf8()..]
.chars()
.next()
.is_some_and(is_name_start)
}
fn current_hash_starts_name(&self) -> bool {
if self.current_char() != Some('#') {
return false;
}
let next_offset = self.offset + '#'.len_utf8();
self.text[next_offset..]
.chars()
.next()
.is_some_and(is_name_continue)
|| self.escape_starts_at(next_offset)
}
fn consume_unexpected(&mut self, char: char) {
let start = self.offset;
self.bump_char(char);
self.push(SyntaxKind::Delim, start, self.offset);
self.error(
ParseErrorCode::UnexpectedCharacter,
start,
self.offset,
"unexpected character",
);
}
fn push(&mut self, kind: SyntaxKind, start: usize, end: usize) {
self.tokens.push(Token {
kind,
text: &self.text[start..end],
range: text_range(start, end),
});
}
fn error(&mut self, code: ParseErrorCode, start: usize, end: usize, message: &'static str) {
self.errors.push(ParseError {
code,
range: text_range(start, end),
message,
});
}
fn starts_with(&self, pattern: &str) -> bool {
self.text[self.offset..].starts_with(pattern)
}
fn current_starts_valid_escape(&self) -> bool {
self.escape_starts_at(self.offset)
}
fn current_starts_number(&self) -> bool {
self.starts_number_at(self.offset)
}
fn current_starts_number_exponent(&self) -> bool {
let Some('e' | 'E') = self.current_char() else {
return false;
};
let exponent_offset = self.offset + 'e'.len_utf8();
self.char_at(exponent_offset)
.is_some_and(|char| char.is_ascii_digit())
|| (matches!(self.char_at(exponent_offset), Some('+' | '-'))
&& self.char_after_offset_is_ascii_digit(exponent_offset))
}
fn starts_number_at(&self, offset: usize) -> bool {
let Some(first) = self.char_at(offset) else {
return false;
};
let second_offset = offset + first.len_utf8();
match first {
'+' | '-' => {
self.char_at(second_offset)
.is_some_and(|char| char.is_ascii_digit())
|| (self.char_at(second_offset) == Some('.')
&& self.char_after_offset_is_ascii_digit(second_offset))
}
'.' => self.char_after_offset_is_ascii_digit(offset),
char => char.is_ascii_digit(),
}
}
fn current_starts_ident_sequence(&self) -> bool {
self.starts_ident_sequence_at(self.offset)
}
fn starts_ident_sequence_at(&self, offset: usize) -> bool {
let Some(first) = self.char_at(offset) else {
return false;
};
let second_offset = offset + first.len_utf8();
match first {
'-' => {
self.char_at(second_offset)
.is_some_and(|char| char == '-' || is_name_start(char))
|| self.escape_starts_at(second_offset)
}
'\\' => self.escape_starts_at(offset),
char => is_name_start(char),
}
}
fn escape_starts_at(&self, offset: usize) -> bool {
if !self
.text
.get(offset..)
.is_some_and(|remaining| remaining.starts_with('\\'))
{
return false;
}
self.text[offset + '\\'.len_utf8()..]
.chars()
.next()
.is_some_and(|char| !matches!(char, '\n' | '\r' | '\u{000c}'))
}
fn char_at(&self, offset: usize) -> Option<char> {
self.text.get(offset..)?.chars().next()
}
fn char_after_current_is_ascii_digit(&self) -> bool {
self.char_after_offset_is_ascii_digit(self.offset)
}
fn char_after_offset_is_ascii_digit(&self, offset: usize) -> bool {
let Some(char) = self.char_at(offset) else {
return false;
};
self.char_at(offset + char.len_utf8())
.is_some_and(|char| char.is_ascii_digit())
}
fn starts_with_ascii_keyword(&self, keyword: &str) -> bool {
let remaining = &self.text[self.offset..];
let Some(prefix) = remaining.get(..keyword.len()) else {
return false;
};
if !prefix.eq_ignore_ascii_case(keyword) {
return false;
}
remaining[keyword.len()..]
.chars()
.next()
.is_none_or(|char| !is_name_continue(char))
}
fn supports_scss_interpolation(&self) -> bool {
matches!(
self.extension.dialect(),
StyleDialect::Scss | StyleDialect::Sass
)
}
fn supports_less_interpolation(&self) -> bool {
self.extension.dialect() == StyleDialect::Less
}
fn starts_less_escaped_string(&self) -> bool {
self.extension.dialect() == StyleDialect::Less
&& (self.starts_with("~\"") || self.starts_with("~'"))
}
fn starts_unicode_range(&self) -> bool {
let mut chars = self.text[self.offset..].chars();
matches!(chars.next(), Some('u' | 'U'))
&& chars.next() == Some('+')
&& chars
.next()
.is_some_and(|char| char.is_ascii_hexdigit() || char == '?')
}
fn current_char(&self) -> Option<char> {
self.text[self.offset..].chars().next()
}
fn next_char_is_hex_digit(&self) -> bool {
let offset = self.offset + '-'.len_utf8();
self.text
.get(offset..)
.and_then(|tail| tail.chars().next())
.is_some_and(|char| char.is_ascii_hexdigit())
}
fn bump_current(&mut self) {
if let Some(char) = self.current_char() {
self.bump_char(char);
}
}
fn bump_char(&mut self, char: char) {
self.offset += char.len_utf8();
}
}
fn public_token_text(text: &str) -> String {
text.chars()
.map(css_syntax_preprocessed_char)
.collect::<String>()
}
fn css_syntax_preprocessed_char(char: char) -> char {
if char == '\0' { '\u{fffd}' } else { char }
}
fn is_name_start(char: char) -> bool {
let char = css_syntax_preprocessed_char(char);
char == '_' || char == '-' || char.is_alphabetic() || !char.is_ascii()
}
fn is_name_continue(char: char) -> bool {
is_name_start(char) || char.is_ascii_digit()
}
fn is_non_printable_code_point(char: char) -> bool {
let char = css_syntax_preprocessed_char(char);
matches!(char, '\u{0000}'..='\u{0008}' | '\u{000b}' | '\u{000e}'..='\u{001f}' | '\u{007f}')
}
fn is_custom_property_name_text(text: &str) -> bool {
let Some(rest) = text.strip_prefix("--") else {
return false;
};
let Some(first) = rest.chars().next() else {
return false;
};
first == '-' || is_name_start(first) || starts_valid_escape_text(rest)
}
fn starts_valid_escape_text(text: &str) -> bool {
text.starts_with('\\')
&& text['\\'.len_utf8()..]
.chars()
.next()
.is_some_and(|char| !matches!(char, '\n' | '\r' | '\u{000c}'))
}
fn is_css_at_rule_name(text: &str) -> bool {
matches_ignore_ascii_case(
text,
&[
"@charset",
"@container",
"@font-face",
"@font-feature-values",
"@font-palette-values",
"@import",
"@keyframes",
"@layer",
"@media",
"@namespace",
"@page",
"@property",
"@scope",
"@starting-style",
"@supports",
"@counter-style",
"@custom-media",
"@color-profile",
"@nest",
"@position-try",
"@view-transition",
"@stylistic",
"@styleset",
"@character-variant",
"@swash",
"@ornaments",
"@annotation",
"@historical-forms",
"@when",
"@else",
],
)
}
fn is_interpolation_start(kind: SyntaxKind) -> bool {
matches!(
kind,
SyntaxKind::ScssInterpolationStart | SyntaxKind::LessInterpolationStart
)
}
fn is_component_value_atom_start(kind: SyntaxKind) -> bool {
matches!(
kind,
SyntaxKind::Ident
| SyntaxKind::CustomPropertyName
| SyntaxKind::Number
| SyntaxKind::Percentage
| SyntaxKind::Dimension
| SyntaxKind::String
| SyntaxKind::LessEscapedString
| SyntaxKind::UnicodeRange
| SyntaxKind::Hash
| SyntaxKind::Url
| SyntaxKind::BadUrl
| SyntaxKind::BadString
| SyntaxKind::Important
| SyntaxKind::ScssVariable
| SyntaxKind::LessVariable
| SyntaxKind::LessPropertyVariableToken
| SyntaxKind::ScssInterpolationStart
| SyntaxKind::LessInterpolationStart
)
}
fn interpolation_end_kind(start_kind: SyntaxKind) -> Option<SyntaxKind> {
match start_kind {
SyntaxKind::ScssInterpolationStart => Some(SyntaxKind::ScssInterpolationEnd),
SyntaxKind::LessInterpolationStart => Some(SyntaxKind::LessInterpolationEnd),
_ => None,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct SelectorBranch {
name: String,
range: TextRange,
bare_suffix_base: bool,
}
fn collect_selector_facts_from_tokens(tokens: &[Token<'_>]) -> Vec<ParsedSelectorFact> {
let mut selectors = Vec::new();
let mut seen = BTreeSet::new();
collect_selector_facts_in_range(
tokens,
0,
tokens.len(),
&[],
None,
&mut seen,
&mut selectors,
);
selectors
}
fn collect_selector_facts_in_range(
tokens: &[Token<'_>],
start: usize,
end: usize,
parent_branches: &[SelectorBranch],
css_module_scope: Option<&'static str>,
seen: &mut BTreeSet<(ParsedSelectorFactKind, String, u32, u32)>,
selectors: &mut Vec<ParsedSelectorFact>,
) {
let mut index = start;
while index < end {
index = skip_trivia_tokens(tokens, index, end);
if index >= end {
break;
}
if tokens[index].kind == SyntaxKind::AtKeyword {
let block = find_block_after_header(tokens, index, end);
if let Some((open, close)) = block {
if tokens[index].text == "@nest" {
if css_module_scope == Some("global") {
collect_selector_facts_in_range(
tokens,
open + 1,
close,
&[],
css_module_scope,
seen,
selectors,
);
} else {
let branches =
resolve_selector_header(tokens, index + 1, open, parent_branches);
push_class_selector_facts_from_header(
selectors,
seen,
tokens,
index + 1,
open,
);
for branch in &branches {
push_selector_fact(
selectors,
seen,
ParsedSelectorFactKind::Class,
branch.name.clone(),
branch.range,
);
}
collect_selector_facts_in_range(
tokens,
open + 1,
close,
&branches,
css_module_scope,
seen,
selectors,
);
}
} else if style_wrapper_at_rule(tokens[index].text) {
collect_selector_facts_in_range(
tokens,
open + 1,
close,
parent_branches,
css_module_scope,
seen,
selectors,
);
}
index = close + 1;
} else {
index = skip_statement(tokens, index, end);
}
continue;
}
let Some((open, close)) = find_block_after_header(tokens, index, end) else {
index = skip_statement(tokens, index, end);
continue;
};
let effective_scope = css_module_scope
.or_else(|| css_module_block_scope_marker_in_header(tokens, index, open));
if effective_scope == Some("global") {
collect_selector_facts_in_range(
tokens,
open + 1,
close,
&[],
effective_scope,
seen,
selectors,
);
} else {
let branches = resolve_selector_header(tokens, index, open, parent_branches);
push_class_selector_facts_from_header(selectors, seen, tokens, index, open);
for branch in &branches {
push_selector_fact(
selectors,
seen,
ParsedSelectorFactKind::Class,
branch.name.clone(),
branch.range,
);
}
for id in collect_id_selector_facts_from_header(tokens, index, open)
.into_iter()
.chain(collect_local_function_id_selector_facts_from_header(
tokens, index, open,
))
{
push_selector_fact(selectors, seen, ParsedSelectorFactKind::Id, id.0, id.1);
}
for placeholder in collect_placeholder_selector_facts_from_header(tokens, index, open) {
push_selector_fact(
selectors,
seen,
ParsedSelectorFactKind::Placeholder,
placeholder.0,
placeholder.1,
);
}
collect_selector_facts_in_range(
tokens,
open + 1,
close,
&branches,
effective_scope,
seen,
selectors,
);
}
index = close + 1;
}
}
fn push_class_selector_facts_from_header(
selectors: &mut Vec<ParsedSelectorFact>,
seen: &mut BTreeSet<(ParsedSelectorFactKind, String, u32, u32)>,
tokens: &[Token<'_>],
start: usize,
end: usize,
) {
for (name, range) in collect_class_selector_names_from_header(tokens, start, end) {
push_selector_fact(selectors, seen, ParsedSelectorFactKind::Class, name, range);
}
}
fn push_selector_fact(
selectors: &mut Vec<ParsedSelectorFact>,
seen: &mut BTreeSet<(ParsedSelectorFactKind, String, u32, u32)>,
kind: ParsedSelectorFactKind,
name: String,
range: TextRange,
) {
if seen.insert((
kind,
name.clone(),
u32::from(range.start()),
u32::from(range.end()),
)) {
selectors.push(ParsedSelectorFact { kind, name, range });
}
}
fn resolve_selector_header(
tokens: &[Token<'_>],
start: usize,
end: usize,
parent_branches: &[SelectorBranch],
) -> Vec<SelectorBranch> {
split_selector_groups(tokens, start, end)
.into_iter()
.flat_map(|(group_start, group_end)| {
resolve_selector_group(tokens, group_start, group_end, parent_branches)
})
.collect()
}
fn resolve_selector_group(
tokens: &[Token<'_>],
start: usize,
end: usize,
parent_branches: &[SelectorBranch],
) -> Vec<SelectorBranch> {
if let Some(mut local_names) = collect_local_function_selector_names(tokens, start, end) {
local_names.extend(collect_class_selector_names_from_header(tokens, start, end));
let bare_suffix_base = parent_branches.is_empty() && local_names.len() == 1;
return local_names
.into_iter()
.map(|(name, range)| SelectorBranch {
name,
range,
bare_suffix_base,
})
.collect();
}
let (tail_start, tail_end) = selector_group_tail_range(tokens, start, end);
let tail_start = skip_trivia_tokens(tokens, tail_start, tail_end);
if let Some((suffix, range)) = ampersand_suffix_selector(tokens, tail_start, tail_end) {
let bases: Vec<&SelectorBranch> = if parent_branches.is_empty() {
Vec::new()
} else {
parent_branches
.iter()
.filter(|parent| parent.bare_suffix_base)
.collect()
};
return bases
.into_iter()
.map(|parent| SelectorBranch {
name: format!("{}{}", parent.name, suffix),
range,
bare_suffix_base: parent.bare_suffix_base,
})
.collect();
}
let class_names = collect_class_selector_names_from_header(tokens, tail_start, tail_end);
if class_names.is_empty() {
return Vec::new();
}
let bare_suffix_base = parent_branches.is_empty()
&& class_names.len() == 1
&& is_bare_class_selector_group(tokens, tail_start, tail_end);
class_names
.into_iter()
.map(|(name, range)| SelectorBranch {
name,
range,
bare_suffix_base,
})
.collect()
}
fn is_bare_class_selector_group(tokens: &[Token<'_>], start: usize, end: usize) -> bool {
let dot_index = skip_trivia_tokens(tokens, start, end);
if tokens.get(dot_index).map(|token| token.kind) != Some(SyntaxKind::Dot) {
return false;
}
let name_index = skip_trivia_tokens(tokens, dot_index + 1, end);
if !tokens.get(name_index).is_some_and(|token| {
matches!(
token.kind,
SyntaxKind::Ident | SyntaxKind::CustomPropertyName
)
}) {
return false;
}
skip_trivia_tokens(tokens, name_index + 1, end) >= end
}
fn split_selector_groups(tokens: &[Token<'_>], start: usize, end: usize) -> Vec<(usize, usize)> {
let mut groups = Vec::new();
let mut group_start = start;
let mut paren_depth = 0usize;
let mut bracket_depth = 0usize;
let mut index = start;
while index < end {
match tokens[index].kind {
SyntaxKind::LeftParen => paren_depth += 1,
SyntaxKind::RightParen => paren_depth = paren_depth.saturating_sub(1),
SyntaxKind::LeftBracket => bracket_depth += 1,
SyntaxKind::RightBracket => bracket_depth = bracket_depth.saturating_sub(1),
SyntaxKind::Comma if paren_depth == 0 && bracket_depth == 0 => {
groups.push((group_start, index));
group_start = index + 1;
}
_ => {}
}
index += 1;
}
groups.push((group_start, end));
groups
}
fn selector_group_tail_range(tokens: &[Token<'_>], start: usize, end: usize) -> (usize, usize) {
let mut paren_depth = 0usize;
let mut bracket_depth = 0usize;
let mut tail_start = start;
let mut index = start;
while index < end {
match tokens[index].kind {
SyntaxKind::LeftParen => paren_depth += 1,
SyntaxKind::RightParen => paren_depth = paren_depth.saturating_sub(1),
SyntaxKind::LeftBracket => bracket_depth += 1,
SyntaxKind::RightBracket => bracket_depth = bracket_depth.saturating_sub(1),
kind if paren_depth == 0 && bracket_depth == 0 && is_selector_combinator_kind(kind) => {
tail_start = index + 1;
}
SyntaxKind::Whitespace if paren_depth == 0 && bracket_depth == 0 => {
let previous = previous_non_trivia_token(tokens, start, index);
let next = next_non_trivia_token_until(tokens, index + 1, end);
if previous.is_some_and(|token| selector_component_can_end(token.kind))
&& next.is_some_and(|token| selector_component_can_start(token.kind))
{
tail_start = index + 1;
}
}
_ => {}
}
index += 1;
}
(tail_start, end)
}
fn ampersand_suffix_selector(
tokens: &[Token<'_>],
start: usize,
end: usize,
) -> Option<(String, TextRange)> {
let ampersand_index = skip_trivia_tokens(tokens, start, end);
if tokens.get(ampersand_index)?.kind != SyntaxKind::Ampersand {
return None;
}
let suffix = next_non_trivia_token_until(tokens, ampersand_index + 1, end)?;
if matches!(
suffix.kind,
SyntaxKind::Ident | SyntaxKind::CustomPropertyName
) {
return Some((suffix.text.to_string(), suffix.range));
}
None
}
fn collect_class_selector_names_from_header(
tokens: &[Token<'_>],
start: usize,
end: usize,
) -> Vec<(String, TextRange)> {
let mut names = Vec::new();
let mut index = start;
let mut paren_depth = 0usize;
let mut bracket_depth = 0usize;
while index < end {
match tokens[index].kind {
SyntaxKind::LeftParen => paren_depth += 1,
SyntaxKind::RightParen => paren_depth = paren_depth.saturating_sub(1),
SyntaxKind::LeftBracket => bracket_depth += 1,
SyntaxKind::RightBracket => bracket_depth = bracket_depth.saturating_sub(1),
_ => {}
}
if paren_depth == 0
&& bracket_depth == 0
&& tokens[index].kind == SyntaxKind::Dot
&& let Some(name) = next_non_trivia_token_until(tokens, index + 1, end)
&& matches!(
name.kind,
SyntaxKind::Ident | SyntaxKind::CustomPropertyName
)
{
names.push((name.text.to_string(), name.range));
}
index += 1;
}
names
}
fn collect_local_function_selector_names(
tokens: &[Token<'_>],
start: usize,
end: usize,
) -> Option<Vec<(String, TextRange)>> {
let colon_index = skip_trivia_tokens(tokens, start, end);
if tokens.get(colon_index)?.kind != SyntaxKind::Colon {
return None;
}
let ident = next_non_trivia_token_until(tokens, colon_index + 1, end)?;
if ident.kind != SyntaxKind::Ident || ident.text != "local" {
return None;
}
let open_index = skip_trivia_tokens(tokens, colon_index + 2, end);
if tokens.get(open_index)?.kind != SyntaxKind::LeftParen {
return None;
}
Some(collect_class_selector_names_from_header(
tokens,
open_index + 1,
end.saturating_sub(1),
))
}
fn collect_local_function_id_selector_facts_from_header(
tokens: &[Token<'_>],
start: usize,
end: usize,
) -> Vec<(String, TextRange)> {
let mut ids = Vec::new();
let mut index = start;
while index < end {
if tokens[index].kind == SyntaxKind::Colon
&& let Some(scope) = next_non_trivia_token_until(tokens, index + 1, end)
&& scope.kind == SyntaxKind::Ident
&& scope.text == "local"
&& let Some(open) = next_non_trivia_token_after_range(tokens, scope.range, end)
&& open.kind == SyntaxKind::LeftParen
&& let Some(close) = matching_right_paren_from_range(tokens, open.range, end)
{
ids.extend(collect_id_selector_facts_from_header(
tokens,
token_index_by_range(tokens, open.range).map_or(index + 1, |value| value + 1),
close,
));
index = close.saturating_add(1);
continue;
}
index += 1;
}
ids
}
fn css_module_block_scope_marker_in_header(
tokens: &[Token<'_>],
start: usize,
end: usize,
) -> Option<&'static str> {
if next_non_trivia_token_until(tokens, start, end)
.is_some_and(|token| token.kind == SyntaxKind::AtKeyword)
{
return None;
}
css_module_scope_marker_after_colon(tokens, start, end)
.filter(|_| !css_module_scope_marker_is_function(tokens, start, end))
}
fn css_module_header_is_global_only(tokens: &[Token<'_>], start: usize, end: usize) -> bool {
if next_non_trivia_token_until(tokens, start, end)
.is_some_and(|token| token.kind == SyntaxKind::AtKeyword)
{
return false;
}
css_module_header_contains_scope(tokens, start, end, "global")
&& collect_class_selector_names_from_header(tokens, start, end).is_empty()
&& collect_local_function_selector_names(tokens, start, end)
.map(|names| names.is_empty())
.unwrap_or(true)
}
fn css_module_header_contains_scope(
tokens: &[Token<'_>],
start: usize,
end: usize,
expected_scope: &str,
) -> bool {
let mut index = start;
while index < end {
if tokens[index].kind == SyntaxKind::Colon
&& let Some(scope) = next_non_trivia_token_until(tokens, index + 1, end)
&& scope.kind == SyntaxKind::Ident
&& scope.text == expected_scope
{
return true;
}
index += 1;
}
false
}
fn css_module_scope_marker_after_colon(
tokens: &[Token<'_>],
start: usize,
end: usize,
) -> Option<&'static str> {
let colon = skip_trivia_tokens(tokens, start, end);
if tokens.get(colon)?.kind != SyntaxKind::Colon {
return None;
}
let scope = next_non_trivia_token_until(tokens, colon + 1, end)?;
if scope.kind != SyntaxKind::Ident {
return None;
}
match scope.text {
"global" => Some("global"),
"local" => Some("local"),
_ => None,
}
}
fn css_module_scope_marker_is_function(tokens: &[Token<'_>], start: usize, end: usize) -> bool {
let colon = skip_trivia_tokens(tokens, start, end);
let mut index = colon + 1;
let Some(scope) = next_non_trivia_token_until(tokens, index, end) else {
return false;
};
while index < end {
if tokens[index].range == scope.range {
break;
}
index += 1;
}
let Some(next) = next_non_trivia_token_until(tokens, index + 1, end) else {
return false;
};
scope.kind == SyntaxKind::Ident && next.kind == SyntaxKind::LeftParen
}
fn collect_id_selector_facts_from_header(
tokens: &[Token<'_>],
start: usize,
end: usize,
) -> Vec<(String, TextRange)> {
let mut names = Vec::new();
let mut index = start;
let mut paren_depth = 0usize;
let mut bracket_depth = 0usize;
while index < end {
match tokens[index].kind {
SyntaxKind::LeftParen => paren_depth += 1,
SyntaxKind::RightParen => paren_depth = paren_depth.saturating_sub(1),
SyntaxKind::LeftBracket => bracket_depth += 1,
SyntaxKind::RightBracket => bracket_depth = bracket_depth.saturating_sub(1),
_ => {}
}
let token = tokens[index];
if paren_depth == 0 && bracket_depth == 0 && token.kind == SyntaxKind::Hash {
names.push((token.text.trim_start_matches('#').to_string(), token.range));
}
index += 1;
}
names
}
fn collect_placeholder_selector_facts_from_header(
tokens: &[Token<'_>],
start: usize,
end: usize,
) -> Vec<(String, TextRange)> {
let mut names = Vec::new();
let mut index = start;
let mut paren_depth = 0usize;
let mut bracket_depth = 0usize;
while index < end {
match tokens[index].kind {
SyntaxKind::LeftParen => paren_depth += 1,
SyntaxKind::RightParen => paren_depth = paren_depth.saturating_sub(1),
SyntaxKind::LeftBracket => bracket_depth += 1,
SyntaxKind::RightBracket => bracket_depth = bracket_depth.saturating_sub(1),
_ => {}
}
let token = tokens[index];
if paren_depth == 0 && bracket_depth == 0 && token.kind == SyntaxKind::ScssPlaceholder {
names.push((token.text.trim_start_matches('%').to_string(), token.range));
}
index += 1;
}
names
}
fn collect_variable_facts_from_tokens(tokens: &[Token<'_>]) -> Vec<ParsedVariableFact> {
let mut variables = Vec::new();
for (index, token) in tokens.iter().enumerate() {
let kind = match token.kind {
SyntaxKind::ScssVariable => {
if scss_variable_token_is_declaration(tokens, index) {
ParsedVariableFactKind::ScssDeclaration
} else {
ParsedVariableFactKind::ScssReference
}
}
SyntaxKind::LessVariable => {
if next_non_trivia_token(tokens, index + 1)
.is_some_and(|candidate| candidate.kind == SyntaxKind::Colon)
{
ParsedVariableFactKind::LessDeclaration
} else {
ParsedVariableFactKind::LessReference
}
}
SyntaxKind::CustomPropertyName => {
if previous_non_trivia_token(tokens, 0, index).is_some_and(|candidate| {
matches!(candidate.kind, SyntaxKind::Ampersand | SyntaxKind::Dot)
}) {
continue;
}
if let Some(at_rule_name) = containing_at_rule_header_name(tokens, index) {
if at_rule_name == "@property" {
ParsedVariableFactKind::CustomPropertyDeclaration
} else {
continue;
}
} else if next_non_trivia_token(tokens, index + 1)
.is_some_and(|candidate| candidate.kind == SyntaxKind::Colon)
{
ParsedVariableFactKind::CustomPropertyDeclaration
} else {
ParsedVariableFactKind::CustomPropertyReference
}
}
_ => continue,
};
variables.push(ParsedVariableFact {
kind,
name: token.text.to_string(),
range: token.range,
});
}
variables
}
fn scss_variable_token_is_declaration(tokens: &[Token<'_>], index: usize) -> bool {
next_non_trivia_token(tokens, index + 1).is_some_and(|candidate| {
candidate.kind == SyntaxKind::Colon
|| (matches!(candidate.kind, SyntaxKind::Comma | SyntaxKind::RightParen)
&& containing_at_rule_header_name(tokens, index).is_some_and(|name| {
name.eq_ignore_ascii_case("@mixin") || name.eq_ignore_ascii_case("@function")
}))
})
}
fn collect_sass_symbol_facts_from_tokens(tokens: &[Token<'_>]) -> Vec<ParsedSassSymbolFact> {
let declared_functions = collect_sass_callable_declaration_names(tokens, "@function");
let mut symbols = Vec::new();
for (index, token) in tokens.iter().enumerate() {
match token.kind {
SyntaxKind::ScssVariable => {
let kind = if scss_variable_token_is_declaration(tokens, index) {
ParsedSassSymbolFactKind::VariableDeclaration
} else {
ParsedSassSymbolFactKind::VariableReference
};
let namespace = (!scss_variable_token_is_declaration(tokens, index))
.then(|| sass_member_namespace_before(tokens, index))
.flatten();
symbols.push(ParsedSassSymbolFact {
kind,
symbol_kind: "variable",
name: token.text.trim_start_matches('$').to_string(),
role: match kind {
ParsedSassSymbolFactKind::VariableDeclaration => "declaration",
_ => "reference",
},
namespace,
range: sass_symbol_variable_range(token, kind),
});
}
SyntaxKind::AtKeyword if token.text.eq_ignore_ascii_case("@mixin") => {
if let Some(name) = sass_callable_name_after_at_rule(tokens, index) {
symbols.push(ParsedSassSymbolFact {
kind: ParsedSassSymbolFactKind::MixinDeclaration,
symbol_kind: "mixin",
name: name.text.to_string(),
role: "declaration",
namespace: None,
range: name.range,
});
}
}
SyntaxKind::AtKeyword if token.text.eq_ignore_ascii_case("@include") => {
if let Some((name, namespace)) = sass_include_name_after_at_rule(tokens, index) {
symbols.push(ParsedSassSymbolFact {
kind: ParsedSassSymbolFactKind::MixinInclude,
symbol_kind: "mixin",
name: name.text.to_string(),
role: "include",
namespace,
range: name.range,
});
}
}
SyntaxKind::AtKeyword if token.text.eq_ignore_ascii_case("@function") => {
if let Some(name) = sass_callable_name_after_at_rule(tokens, index) {
symbols.push(ParsedSassSymbolFact {
kind: ParsedSassSymbolFactKind::FunctionDeclaration,
symbol_kind: "function",
name: name.text.to_string(),
role: "declaration",
namespace: None,
range: name.range,
});
}
}
SyntaxKind::Ident
if (declared_functions.contains(token.text)
|| sass_member_namespace_before(tokens, index).is_some())
&& next_non_trivia_token(tokens, index + 1)
.is_some_and(|candidate| candidate.kind == SyntaxKind::LeftParen)
&& !containing_at_rule_header_name(tokens, index)
.is_some_and(|name| name.eq_ignore_ascii_case("@include"))
&& previous_non_trivia_token(tokens, 0, index).is_none_or(|candidate| {
!matches!(candidate.kind, SyntaxKind::AtKeyword)
}) =>
{
symbols.push(ParsedSassSymbolFact {
kind: ParsedSassSymbolFactKind::FunctionCall,
symbol_kind: "function",
name: token.text.to_string(),
role: "call",
namespace: sass_member_namespace_before(tokens, index),
range: token.range,
});
}
_ => {}
}
}
symbols
}
fn sass_symbol_variable_range(token: &Token<'_>, kind: ParsedSassSymbolFactKind) -> TextRange {
if kind == ParsedSassSymbolFactKind::VariableDeclaration && token.text.starts_with('$') {
let start = u32::from(token.range.start());
let end = u32::from(token.range.end());
if start < end {
return TextRange::new(TextSize::from(start + 1), TextSize::from(end));
}
}
token.range
}
fn collect_sass_callable_declaration_names(
tokens: &[Token<'_>],
at_keyword: &str,
) -> BTreeSet<String> {
tokens
.iter()
.enumerate()
.filter_map(|(index, token)| {
(token.kind == SyntaxKind::AtKeyword && token.text.eq_ignore_ascii_case(at_keyword))
.then(|| sass_callable_name_after_at_rule(tokens, index))
.flatten()
.map(|name| name.text.to_string())
})
.collect()
}
fn sass_callable_name_after_at_rule<'text>(
tokens: &[Token<'text>],
at_rule_index: usize,
) -> Option<Token<'text>> {
let statement_end = css_module_value_statement_end(tokens, at_rule_index + 1);
let name_index = next_non_trivia_token_index_until(tokens, at_rule_index + 1, statement_end)?;
let name = tokens[name_index];
if name.kind != SyntaxKind::Ident {
return None;
}
if next_non_trivia_token_index_until(tokens, name_index + 1, statement_end)
.is_some_and(|next| tokens[next].kind == SyntaxKind::Dot)
{
return None;
}
Some(name)
}
fn sass_include_name_after_at_rule<'text>(
tokens: &[Token<'text>],
at_rule_index: usize,
) -> Option<(Token<'text>, Option<String>)> {
let statement_end = css_module_value_statement_end(tokens, at_rule_index + 1);
let first_index = next_non_trivia_token_index_until(tokens, at_rule_index + 1, statement_end)?;
let first = tokens[first_index];
if first.kind != SyntaxKind::Ident {
return None;
}
let Some(dot_index) = next_non_trivia_token_index_until(tokens, first_index + 1, statement_end)
else {
return Some((first, None));
};
if tokens[dot_index].kind != SyntaxKind::Dot {
return Some((first, None));
}
let member_index = next_non_trivia_token_index_until(tokens, dot_index + 1, statement_end)?;
let member = tokens[member_index];
(member.kind == SyntaxKind::Ident).then(|| (member, Some(first.text.to_string())))
}
fn sass_member_namespace_before(tokens: &[Token<'_>], member_index: usize) -> Option<String> {
let dot_index = previous_non_trivia_token_index(tokens, member_index, 0)?;
if tokens[dot_index].kind != SyntaxKind::Dot {
return None;
}
let namespace = tokens[previous_non_trivia_token_index(tokens, dot_index, 0)?];
(namespace.kind == SyntaxKind::Ident).then(|| namespace.text.to_string())
}
fn collect_sass_include_facts_from_tokens(
source: &str,
tokens: &[Token<'_>],
) -> Vec<ParsedSassIncludeFact> {
let mut includes = Vec::new();
for (index, token) in tokens.iter().enumerate() {
if token.kind != SyntaxKind::AtKeyword || !token.text.eq_ignore_ascii_case("@include") {
continue;
}
let statement_end = css_module_value_statement_end(tokens, index + 1);
let Some((name, namespace)) = sass_include_name_after_at_rule(tokens, index) else {
continue;
};
let header_end = previous_non_trivia_token_index(tokens, statement_end, index + 1)
.map(|previous| tokens[previous].range.end())
.unwrap_or(name.range.end());
let params = source
.get(u32::from(name.range.end()) as usize..u32::from(header_end) as usize)
.unwrap_or_default()
.trim()
.to_string();
includes.push(ParsedSassIncludeFact {
name: name.text.to_string(),
namespace,
params,
range: TextRange::new(token.range.start(), header_end),
});
}
includes
}
fn collect_sass_module_edge_facts_from_tokens(
tokens: &[Token<'_>],
) -> Vec<ParsedSassModuleEdgeFact> {
let mut edges = Vec::new();
let mut seen = BTreeSet::new();
for (index, token) in tokens.iter().enumerate() {
if token.kind != SyntaxKind::AtKeyword {
continue;
}
let Some(kind) = sass_module_edge_kind(token.text) else {
continue;
};
let start = skip_trivia_tokens(tokens, index + 1, tokens.len());
let end = css_module_value_statement_end(tokens, start);
if kind == ParsedSassModuleEdgeFactKind::Import {
collect_sass_import_module_edges(tokens, start, end, &mut edges, &mut seen);
continue;
}
let Some(source_index) = next_non_trivia_token_index_until(tokens, start, end) else {
continue;
};
let source = tokens[source_index];
if !matches!(source.kind, SyntaxKind::String | SyntaxKind::Url) {
continue;
}
let source_name = css_module_value_source_name(source);
let (namespace_kind, namespace) = if kind == ParsedSassModuleEdgeFactKind::Use {
sass_module_use_namespace(tokens, source_name.as_str(), source_index + 1, end)
} else {
(None, None)
};
let (visibility_filter_kind, visibility_filter_names) =
if kind == ParsedSassModuleEdgeFactKind::Forward {
sass_module_forward_visibility_filter(tokens, source_index + 1, end)
} else {
(None, Vec::new())
};
let forward_prefix = if kind == ParsedSassModuleEdgeFactKind::Forward {
sass_module_forward_prefix(tokens, source_index + 1, end)
} else {
None
};
push_sass_module_edge_fact(
&mut edges,
&mut seen,
ParsedSassModuleEdgeFact {
kind,
source: source_name,
namespace_kind,
namespace,
forward_prefix,
visibility_filter_kind,
visibility_filter_names,
range: source.range,
},
);
}
edges
}
fn sass_module_edge_kind(text: &str) -> Option<ParsedSassModuleEdgeFactKind> {
match text {
text if text.eq_ignore_ascii_case("@use") => Some(ParsedSassModuleEdgeFactKind::Use),
text if text.eq_ignore_ascii_case("@forward") => {
Some(ParsedSassModuleEdgeFactKind::Forward)
}
text if text.eq_ignore_ascii_case("@import") => Some(ParsedSassModuleEdgeFactKind::Import),
_ => None,
}
}
fn collect_sass_import_module_edges(
tokens: &[Token<'_>],
start: usize,
end: usize,
edges: &mut Vec<ParsedSassModuleEdgeFact>,
seen: &mut BTreeSet<(ParsedSassModuleEdgeFactKind, String, u32, u32)>,
) {
for token in &tokens[start..end] {
if !matches!(token.kind, SyntaxKind::String | SyntaxKind::Url) {
continue;
}
push_sass_module_edge_fact(
edges,
seen,
ParsedSassModuleEdgeFact {
kind: ParsedSassModuleEdgeFactKind::Import,
source: css_module_value_source_name(*token),
namespace_kind: None,
namespace: None,
forward_prefix: None,
visibility_filter_kind: None,
visibility_filter_names: Vec::new(),
range: token.range,
},
);
}
}
fn sass_module_use_namespace(
tokens: &[Token<'_>],
source: &str,
start: usize,
end: usize,
) -> (Option<&'static str>, Option<String>) {
let Some(as_index) = top_level_token_text_index(tokens, start, end, "as") else {
return (
Some("default"),
sass_module_default_namespace(source).map(str::to_string),
);
};
let Some(namespace_index) = next_non_trivia_token_index_until(tokens, as_index + 1, end) else {
return (Some("invalid"), None);
};
let namespace = tokens[namespace_index];
match namespace.kind {
SyntaxKind::Star => (Some("wildcard"), None),
SyntaxKind::Ident => (Some("alias"), Some(namespace.text.to_string())),
_ => (Some("invalid"), None),
}
}
fn sass_module_forward_prefix(tokens: &[Token<'_>], start: usize, end: usize) -> Option<String> {
let as_index = top_level_token_text_index(tokens, start, end, "as")?;
let prefix_index = next_non_trivia_token_index_until(tokens, as_index + 1, end)?;
let prefix = tokens[prefix_index].text.trim();
if prefix.is_empty() {
return None;
}
Some(prefix.to_string())
}
fn sass_module_forward_visibility_filter(
tokens: &[Token<'_>],
start: usize,
end: usize,
) -> (Option<&'static str>, Vec<String>) {
let show_index = top_level_token_text_index(tokens, start, end, "show");
let hide_index = top_level_token_text_index(tokens, start, end, "hide");
let (filter_kind, filter_index) = match (show_index, hide_index) {
(Some(show_index), Some(hide_index)) if show_index <= hide_index => ("show", show_index),
(Some(_), Some(hide_index)) => ("hide", hide_index),
(Some(show_index), None) => ("show", show_index),
(None, Some(hide_index)) => ("hide", hide_index),
(None, None) => return (None, Vec::new()),
};
let clause_end =
top_level_token_text_index(tokens, filter_index + 1, end, "with").unwrap_or(end);
(
Some(filter_kind),
sass_module_visibility_filter_names(tokens, filter_index + 1, clause_end),
)
}
fn sass_module_visibility_filter_names(
tokens: &[Token<'_>],
start: usize,
end: usize,
) -> Vec<String> {
let mut names = BTreeSet::new();
for token in &tokens[start..end] {
match token.kind {
SyntaxKind::Ident | SyntaxKind::ScssVariable => {
if matches_ignore_ascii_case(token.text, &["show", "hide", "with", "as"]) {
continue;
}
let name = token.text.trim_start_matches('$');
if !name.is_empty() {
names.insert(name.to_string());
}
}
_ => {}
}
}
names.into_iter().collect()
}
fn sass_module_default_namespace(source: &str) -> Option<&str> {
let basename = source
.rsplit(['/', '\\', ':'])
.next()
.unwrap_or(source)
.trim_start_matches('_');
let namespace = basename.split('.').next().unwrap_or(basename);
(!namespace.is_empty()).then_some(namespace)
}
fn push_sass_module_edge_fact(
edges: &mut Vec<ParsedSassModuleEdgeFact>,
seen: &mut BTreeSet<(ParsedSassModuleEdgeFactKind, String, u32, u32)>,
edge: ParsedSassModuleEdgeFact,
) {
let start: u32 = edge.range.start().into();
let end: u32 = edge.range.end().into();
if seen.insert((edge.kind, edge.source.clone(), start, end)) {
edges.push(edge);
}
}
fn collect_css_module_value_facts_from_tokens(
tokens: &[Token<'_>],
) -> Vec<ParsedCssModuleValueFact> {
let mut values = Vec::new();
let mut seen = BTreeSet::new();
let value_path_aliases = collect_css_module_value_path_aliases_from_tokens(tokens);
for (index, token) in tokens.iter().enumerate() {
if token.kind != SyntaxKind::AtKeyword || !token.text.eq_ignore_ascii_case("@value") {
continue;
}
let start = skip_trivia_tokens(tokens, index + 1, tokens.len());
let end = css_module_value_statement_end(tokens, start);
let colon_index = top_level_token_kind_index(tokens, start, end, SyntaxKind::Colon);
let from_index = top_level_token_text_index(tokens, start, end, "from");
if let Some(from_index) = from_index
&& match colon_index {
Some(colon_index) => from_index < colon_index,
None => true,
}
{
collect_css_module_value_import_facts(
tokens,
start,
from_index,
end,
&value_path_aliases,
&mut values,
&mut seen,
);
continue;
}
if let Some(colon_index) = colon_index {
if css_module_value_path_alias_from_tokens(tokens, start, colon_index, end).is_some() {
continue;
}
collect_css_module_value_definition_facts(
tokens,
start,
colon_index,
&mut values,
&mut seen,
);
collect_css_module_value_reference_facts(
tokens,
colon_index + 1,
end,
&mut values,
&mut seen,
);
} else {
collect_css_module_value_definition_facts(tokens, start, end, &mut values, &mut seen);
}
}
let local_value_names = values
.iter()
.filter(|value| value.kind == ParsedCssModuleValueFactKind::Definition)
.map(|value| value.name.clone())
.collect::<BTreeSet<_>>();
collect_css_module_value_declaration_reference_facts(
tokens,
0,
tokens.len(),
&local_value_names,
&mut values,
&mut seen,
);
values
}
fn collect_css_module_value_path_aliases_from_tokens(
tokens: &[Token<'_>],
) -> BTreeMap<String, String> {
let mut aliases = BTreeMap::new();
for (index, token) in tokens.iter().enumerate() {
if token.kind != SyntaxKind::AtKeyword || !token.text.eq_ignore_ascii_case("@value") {
continue;
}
let start = skip_trivia_tokens(tokens, index + 1, tokens.len());
let end = css_module_value_statement_end(tokens, start);
let Some(colon_index) = top_level_token_kind_index(tokens, start, end, SyntaxKind::Colon)
else {
continue;
};
if top_level_token_text_index(tokens, start, end, "from").is_some() {
continue;
}
if let Some((name, target)) =
css_module_value_path_alias_from_tokens(tokens, start, colon_index, end)
{
aliases.insert(name, target);
}
}
aliases
}
fn css_module_value_path_alias_from_tokens(
tokens: &[Token<'_>],
start: usize,
colon_index: usize,
end: usize,
) -> Option<(String, String)> {
let name_index = next_non_trivia_token_index_until(tokens, start, colon_index)?;
let name_token = tokens[name_index];
if !css_module_value_name_token_can_define(name_token) {
return None;
}
let source_index = next_non_trivia_token_index_until(tokens, colon_index + 1, end)?;
let source_token = tokens[source_index];
if !matches!(source_token.kind, SyntaxKind::String | SyntaxKind::Url) {
return None;
}
let source = css_module_value_source_name(source_token);
css_module_value_source_looks_like_style_request(&source)
.then(|| (name_token.text.to_string(), source))
}
fn css_module_value_statement_end(tokens: &[Token<'_>], start: usize) -> usize {
let mut index = start;
let mut paren_depth = 0usize;
let mut bracket_depth = 0usize;
while index < tokens.len() {
match tokens[index].kind {
SyntaxKind::LeftParen => paren_depth += 1,
SyntaxKind::RightParen => paren_depth = paren_depth.saturating_sub(1),
SyntaxKind::LeftBracket => bracket_depth += 1,
SyntaxKind::RightBracket => bracket_depth = bracket_depth.saturating_sub(1),
SyntaxKind::Semicolon
| SyntaxKind::SassOptionalSemicolon
| SyntaxKind::LeftBrace
| SyntaxKind::RightBrace
| SyntaxKind::SassIndent
| SyntaxKind::SassDedent
if paren_depth == 0 && bracket_depth == 0 =>
{
return index;
}
_ => {}
}
index += 1;
}
index
}
fn collect_css_module_value_import_facts(
tokens: &[Token<'_>],
start: usize,
from_index: usize,
end: usize,
value_path_aliases: &BTreeMap<String, String>,
values: &mut Vec<ParsedCssModuleValueFact>,
seen: &mut BTreeSet<(ParsedCssModuleValueFactKind, String, u32, u32)>,
) {
collect_css_module_value_import_names(tokens, start, from_index, values, seen);
if let Some((source_name, source_range)) =
css_module_value_import_edge_source(tokens, from_index + 1, end, value_path_aliases)
{
push_css_module_value_fact(
values,
seen,
ParsedCssModuleValueFactKind::ImportSource,
source_name,
source_range,
);
}
}
fn collect_css_module_value_import_edge_facts_from_tokens(
tokens: &[Token<'_>],
) -> Vec<ParsedCssModuleValueImportEdgeFact> {
let mut edges = Vec::new();
let value_path_aliases = collect_css_module_value_path_aliases_from_tokens(tokens);
for (index, token) in tokens.iter().enumerate() {
if token.kind != SyntaxKind::AtKeyword || !token.text.eq_ignore_ascii_case("@value") {
continue;
}
let start = skip_trivia_tokens(tokens, index + 1, tokens.len());
let end = css_module_value_statement_end(tokens, start);
let colon_index = top_level_token_kind_index(tokens, start, end, SyntaxKind::Colon);
let from_index = top_level_token_text_index(tokens, start, end, "from");
let Some(from_index) = from_index else {
continue;
};
if colon_index.is_some_and(|colon_index| from_index > colon_index) {
continue;
}
let Some((import_source, _source_range)) =
css_module_value_import_edge_source(tokens, from_index + 1, end, &value_path_aliases)
else {
continue;
};
collect_css_module_value_import_edges(tokens, start, from_index, import_source, &mut edges);
}
edges
}
fn collect_css_module_value_definition_edge_facts_from_tokens(
tokens: &[Token<'_>],
) -> Vec<ParsedCssModuleValueDefinitionEdgeFact> {
let mut edges = Vec::new();
for (index, token) in tokens.iter().enumerate() {
if token.kind != SyntaxKind::AtKeyword || !token.text.eq_ignore_ascii_case("@value") {
continue;
}
let start = skip_trivia_tokens(tokens, index + 1, tokens.len());
let end = css_module_value_statement_end(tokens, start);
let colon_index = top_level_token_kind_index(tokens, start, end, SyntaxKind::Colon);
let from_index = top_level_token_text_index(tokens, start, end, "from");
let Some(colon_index) = colon_index else {
continue;
};
if from_index.is_some_and(|from_index| from_index < colon_index) {
continue;
}
let definition_names = collect_css_module_value_definition_edge_names(
tokens,
start,
colon_index,
|tokens, index| css_module_value_name_token_can_define(tokens[index]),
);
let reference_names = collect_css_module_value_definition_edge_names(
tokens,
colon_index + 1,
end,
css_module_value_reference_token_can_be_name,
);
if reference_names.is_empty() {
continue;
}
let range_end = end
.checked_sub(1)
.and_then(|end| tokens.get(end))
.map(|token| token.range.end())
.unwrap_or_else(|| tokens[index].range.end());
for definition_name in definition_names {
edges.push(ParsedCssModuleValueDefinitionEdgeFact {
definition_name,
reference_names: reference_names.clone(),
range: TextRange::new(tokens[index].range.start(), range_end),
});
}
}
edges
}
fn collect_css_module_value_definition_edge_names(
tokens: &[Token<'_>],
start: usize,
end: usize,
predicate: impl Fn(&[Token<'_>], usize) -> bool,
) -> Vec<String> {
let mut names = Vec::new();
let mut index = start;
while index < end {
if predicate(tokens, index) && !names.iter().any(|name| name == tokens[index].text) {
names.push(tokens[index].text.to_string());
}
index += 1;
}
names
}
fn css_module_value_import_edge_source(
tokens: &[Token<'_>],
start: usize,
end: usize,
value_path_aliases: &BTreeMap<String, String>,
) -> Option<(String, TextRange)> {
let source_index = next_non_trivia_token_index_until(tokens, start, end)?;
let token = tokens[source_index];
if matches!(token.kind, SyntaxKind::String | SyntaxKind::Url) {
return Some((css_module_value_source_name(token), token.range));
}
if css_module_value_name_token_can_define(token) {
return css_module_value_source_alias_target(token.text, token.range, value_path_aliases);
}
None
}
fn css_module_value_source_alias_target(
name: &str,
range: TextRange,
value_path_aliases: &BTreeMap<String, String>,
) -> Option<(String, TextRange)> {
value_path_aliases
.get(name)
.map(|source| (source.clone(), range))
}
fn collect_css_module_value_import_edges(
tokens: &[Token<'_>],
start: usize,
end: usize,
import_source: String,
edges: &mut Vec<ParsedCssModuleValueImportEdgeFact>,
) {
let mut index = start;
while index < end {
let token = tokens[index];
if !css_module_value_name_token_can_define(token) {
index += 1;
continue;
}
if previous_non_trivia_token_index(tokens, index, start)
.is_some_and(|previous| tokens[previous].text == "as")
{
index += 1;
continue;
}
let remote_name = token.text.to_string();
let mut local_name = remote_name.clone();
let mut local_range = token.range;
if let Some(as_index) = next_non_trivia_token_index_until(tokens, index + 1, end)
&& tokens[as_index].text == "as"
&& let Some(local_index) = next_non_trivia_token_index_until(tokens, as_index + 1, end)
&& css_module_value_name_token_can_define(tokens[local_index])
{
local_name = tokens[local_index].text.to_string();
local_range = tokens[local_index].range;
index = local_index + 1;
} else {
index += 1;
}
edges.push(ParsedCssModuleValueImportEdgeFact {
remote_name,
local_name,
import_source: import_source.clone(),
local_range,
remote_range: token.range,
range: token.range,
});
}
}
fn collect_css_module_value_import_names(
tokens: &[Token<'_>],
start: usize,
end: usize,
values: &mut Vec<ParsedCssModuleValueFact>,
seen: &mut BTreeSet<(ParsedCssModuleValueFactKind, String, u32, u32)>,
) {
let mut index = start;
while index < end {
let token = tokens[index];
if css_module_value_name_token_can_define(token) {
let previous = previous_non_trivia_token_index(tokens, index, start);
let next = next_non_trivia_token_index_until(tokens, index + 1, end);
let kind = if previous.is_some_and(|previous| tokens[previous].text == "as") {
Some(ParsedCssModuleValueFactKind::Definition)
} else if next.is_some_and(|next| tokens[next].text == "as") {
Some(ParsedCssModuleValueFactKind::Reference)
} else {
Some(ParsedCssModuleValueFactKind::Definition)
};
if let Some(kind) = kind {
push_css_module_value_fact(values, seen, kind, token.text.to_string(), token.range);
}
}
index += 1;
}
}
fn collect_css_module_value_definition_facts(
tokens: &[Token<'_>],
start: usize,
end: usize,
values: &mut Vec<ParsedCssModuleValueFact>,
seen: &mut BTreeSet<(ParsedCssModuleValueFactKind, String, u32, u32)>,
) {
let mut index = start;
while index < end {
let token = tokens[index];
if css_module_value_name_token_can_define(token) {
push_css_module_value_fact(
values,
seen,
ParsedCssModuleValueFactKind::Definition,
token.text.to_string(),
token.range,
);
}
index += 1;
}
}
fn collect_css_module_value_reference_facts(
tokens: &[Token<'_>],
start: usize,
end: usize,
values: &mut Vec<ParsedCssModuleValueFact>,
seen: &mut BTreeSet<(ParsedCssModuleValueFactKind, String, u32, u32)>,
) {
let mut index = start;
let mut paren_depth = 0usize;
let mut bracket_depth = 0usize;
while index < end {
match tokens[index].kind {
SyntaxKind::LeftParen => paren_depth += 1,
SyntaxKind::RightParen => paren_depth = paren_depth.saturating_sub(1),
SyntaxKind::LeftBracket => bracket_depth += 1,
SyntaxKind::RightBracket => bracket_depth = bracket_depth.saturating_sub(1),
_ => {}
}
if paren_depth == 0
&& bracket_depth == 0
&& css_module_value_reference_token_can_be_name(tokens, index)
{
push_css_module_value_fact(
values,
seen,
ParsedCssModuleValueFactKind::Reference,
tokens[index].text.to_string(),
tokens[index].range,
);
}
index += 1;
}
}
fn collect_css_module_value_declaration_reference_facts(
tokens: &[Token<'_>],
start: usize,
end: usize,
local_value_names: &BTreeSet<String>,
values: &mut Vec<ParsedCssModuleValueFact>,
seen: &mut BTreeSet<(ParsedCssModuleValueFactKind, String, u32, u32)>,
) {
if local_value_names.is_empty() {
return;
}
let mut index = start;
while index < end {
index = skip_trivia_tokens(tokens, index, end);
if index >= end {
break;
}
if tokens[index].kind == SyntaxKind::AtKeyword {
let block = find_block_after_header(tokens, index, end);
if let Some((open, close)) = block {
if style_wrapper_at_rule(tokens[index].text) {
collect_css_module_value_declaration_reference_facts(
tokens,
open + 1,
close,
local_value_names,
values,
seen,
);
}
index = close + 1;
} else {
index = skip_statement(tokens, index, end);
}
continue;
}
let statement_end = css_module_value_statement_end(tokens, index);
if statement_end < end && tokens[statement_end].kind == SyntaxKind::LeftBrace {
if let Some(close) = matching_right_brace(tokens, statement_end, end) {
collect_css_module_value_declaration_reference_facts(
tokens,
statement_end + 1,
close,
local_value_names,
values,
seen,
);
index = close + 1;
} else {
index = statement_end + 1;
}
continue;
}
if let Some(colon_index) = declaration_colon_index(tokens, index, statement_end.min(end)) {
collect_known_css_module_value_reference_facts(
tokens,
colon_index + 1,
statement_end.min(end),
local_value_names,
values,
seen,
);
}
if statement_end >= end || tokens[statement_end].kind == SyntaxKind::RightBrace {
break;
}
index = statement_end + 1;
}
}
fn declaration_colon_index(tokens: &[Token<'_>], start: usize, end: usize) -> Option<usize> {
let colon_index = top_level_token_kind_index(tokens, start, end, SyntaxKind::Colon)?;
let property_index = previous_non_trivia_token_index(tokens, colon_index, start)?;
if !matches!(
tokens[property_index].kind,
SyntaxKind::Ident
| SyntaxKind::CustomPropertyName
| SyntaxKind::ScssVariable
| SyntaxKind::LessVariable
| SyntaxKind::LessPropertyVariableToken
) {
return None;
}
let value_index = next_non_trivia_token_index_until(tokens, colon_index + 1, end)?;
if matches!(
tokens[value_index].kind,
SyntaxKind::LeftBrace | SyntaxKind::LeftParen | SyntaxKind::LeftBracket
) {
return None;
}
Some(colon_index)
}
fn collect_known_css_module_value_reference_facts(
tokens: &[Token<'_>],
start: usize,
end: usize,
local_value_names: &BTreeSet<String>,
values: &mut Vec<ParsedCssModuleValueFact>,
seen: &mut BTreeSet<(ParsedCssModuleValueFactKind, String, u32, u32)>,
) {
let mut index = start;
let mut paren_depth = 0usize;
let mut bracket_depth = 0usize;
while index < end {
match tokens[index].kind {
SyntaxKind::LeftParen => paren_depth += 1,
SyntaxKind::RightParen => paren_depth = paren_depth.saturating_sub(1),
SyntaxKind::LeftBracket => bracket_depth += 1,
SyntaxKind::RightBracket => bracket_depth = bracket_depth.saturating_sub(1),
_ => {}
}
if paren_depth == 0
&& bracket_depth == 0
&& css_module_value_reference_token_can_be_name(tokens, index)
&& local_value_names.contains(tokens[index].text)
{
push_css_module_value_fact(
values,
seen,
ParsedCssModuleValueFactKind::Reference,
tokens[index].text.to_string(),
tokens[index].range,
);
}
index += 1;
}
}
fn push_css_module_value_fact(
values: &mut Vec<ParsedCssModuleValueFact>,
seen: &mut BTreeSet<(ParsedCssModuleValueFactKind, String, u32, u32)>,
kind: ParsedCssModuleValueFactKind,
name: String,
range: TextRange,
) {
if seen.insert((
kind,
name.clone(),
u32::from(range.start()),
u32::from(range.end()),
)) {
values.push(ParsedCssModuleValueFact { kind, name, range });
}
}
fn top_level_token_kind_index(
tokens: &[Token<'_>],
start: usize,
end: usize,
expected: SyntaxKind,
) -> Option<usize> {
let mut index = start;
let mut paren_depth = 0usize;
let mut bracket_depth = 0usize;
while index < end {
match tokens[index].kind {
SyntaxKind::LeftParen => paren_depth += 1,
SyntaxKind::RightParen => paren_depth = paren_depth.saturating_sub(1),
SyntaxKind::LeftBracket => bracket_depth += 1,
SyntaxKind::RightBracket => bracket_depth = bracket_depth.saturating_sub(1),
kind if kind == expected && paren_depth == 0 && bracket_depth == 0 => {
return Some(index);
}
_ => {}
}
index += 1;
}
None
}
fn top_level_token_text_index(
tokens: &[Token<'_>],
start: usize,
end: usize,
expected: &str,
) -> Option<usize> {
let mut index = start;
let mut paren_depth = 0usize;
let mut bracket_depth = 0usize;
while index < end {
match tokens[index].kind {
SyntaxKind::LeftParen => paren_depth += 1,
SyntaxKind::RightParen => paren_depth = paren_depth.saturating_sub(1),
SyntaxKind::LeftBracket => bracket_depth += 1,
SyntaxKind::RightBracket => bracket_depth = bracket_depth.saturating_sub(1),
SyntaxKind::Ident
if paren_depth == 0
&& bracket_depth == 0
&& tokens[index].text.eq_ignore_ascii_case(expected) =>
{
return Some(index);
}
_ => {}
}
index += 1;
}
None
}
fn previous_non_trivia_token_index(
tokens: &[Token<'_>],
mut index: usize,
start: usize,
) -> Option<usize> {
while index > start {
index -= 1;
if !tokens[index].kind.is_trivia() {
return Some(index);
}
}
None
}
fn css_module_value_name_token_can_define(token: Token<'_>) -> bool {
matches!(
token.kind,
SyntaxKind::Ident | SyntaxKind::CustomPropertyName
) && !matches!(token.text, "as" | "from")
}
fn css_module_value_reference_token_can_be_name(tokens: &[Token<'_>], index: usize) -> bool {
let token = tokens[index];
if !matches!(
token.kind,
SyntaxKind::Ident | SyntaxKind::CustomPropertyName
) {
return false;
}
if let Some(next_index) = next_non_trivia_token_index_until(tokens, index + 1, tokens.len())
&& tokens[next_index].kind == SyntaxKind::LeftParen
{
return false;
}
!css_module_value_literal_ident_is_not_reference(token.text)
}
fn css_module_value_literal_ident_is_not_reference(name: &str) -> bool {
matches!(
name.to_ascii_lowercase().as_str(),
"initial"
| "inherit"
| "unset"
| "revert"
| "revert-layer"
| "none"
| "auto"
| "normal"
| "transparent"
| "currentcolor"
| "black"
| "white"
| "red"
| "green"
| "blue"
| "yellow"
| "magenta"
| "cyan"
| "solid"
| "dashed"
| "block"
| "inline"
| "flex"
| "grid"
)
}
fn css_module_value_source_name(token: Token<'_>) -> String {
token
.text
.trim_matches(|character| character == '"' || character == '\'')
.to_string()
}
fn css_module_value_source_looks_like_style_request(source: &str) -> bool {
let lower = source.to_ascii_lowercase();
(lower.starts_with('/') || lower.starts_with("./") || lower.starts_with("../"))
&& (lower.ends_with(".css")
|| lower.ends_with(".scss")
|| lower.ends_with(".sass")
|| lower.ends_with(".less"))
}
fn collect_css_module_composes_facts_from_tokens(
tokens: &[Token<'_>],
) -> Vec<ParsedCssModuleComposesFact> {
let mut composes = Vec::new();
let mut seen = BTreeSet::new();
for (index, token) in tokens.iter().enumerate() {
if token.kind != SyntaxKind::Ident || !token.text.eq_ignore_ascii_case("composes") {
continue;
}
let Some(colon_index) = next_non_trivia_token_index_until(tokens, index + 1, tokens.len())
else {
continue;
};
if tokens[colon_index].kind != SyntaxKind::Colon {
continue;
}
let start = colon_index + 1;
let end = css_module_value_statement_end(tokens, start);
let from_index = top_level_token_text_index(tokens, start, end, "from");
let target_end = from_index.unwrap_or(end);
collect_css_module_composes_targets(tokens, start, target_end, &mut composes, &mut seen);
if let Some(from_index) = from_index {
collect_css_module_composes_import_source(
tokens,
from_index + 1,
end,
&mut composes,
&mut seen,
);
}
}
composes
}
fn collect_css_module_composes_edge_facts_from_tokens(
tokens: &[Token<'_>],
) -> Vec<ParsedCssModuleComposesEdgeFact> {
let mut edges = Vec::new();
collect_css_module_composes_edge_facts_in_range(tokens, 0, tokens.len(), &[], None, &mut edges);
edges
}
fn collect_css_module_composes_edge_facts_in_range(
tokens: &[Token<'_>],
start: usize,
end: usize,
parent_branches: &[SelectorBranch],
css_module_scope: Option<&'static str>,
edges: &mut Vec<ParsedCssModuleComposesEdgeFact>,
) {
let mut index = start;
while index < end {
index = skip_trivia_tokens(tokens, index, end);
if index >= end {
break;
}
if tokens[index].kind == SyntaxKind::AtKeyword {
let block = find_block_after_header(tokens, index, end);
if let Some((open, close)) = block {
if tokens[index].text == "@nest" {
if css_module_scope == Some("global") {
collect_css_module_composes_edge_facts_in_range(
tokens,
open + 1,
close,
&[],
css_module_scope,
edges,
);
} else {
let branches =
resolve_selector_header(tokens, index + 1, open, parent_branches);
collect_immediate_css_module_composes_edge_facts(
tokens,
open + 1,
close,
&branches,
edges,
);
collect_css_module_composes_edge_facts_in_range(
tokens,
open + 1,
close,
&branches,
css_module_scope,
edges,
);
}
} else if style_wrapper_at_rule(tokens[index].text) {
collect_css_module_composes_edge_facts_in_range(
tokens,
open + 1,
close,
parent_branches,
css_module_scope,
edges,
);
}
index = close + 1;
} else {
index = skip_statement(tokens, index, end);
}
continue;
}
let Some((open, close)) = find_block_after_header(tokens, index, end) else {
index = skip_statement(tokens, index, end);
continue;
};
let effective_scope = css_module_scope
.or_else(|| css_module_block_scope_marker_in_header(tokens, index, open));
if effective_scope == Some("global") {
collect_css_module_composes_edge_facts_in_range(
tokens,
open + 1,
close,
&[],
effective_scope,
edges,
);
} else {
let branches = resolve_selector_header(tokens, index, open, parent_branches);
collect_immediate_css_module_composes_edge_facts(
tokens,
open + 1,
close,
&branches,
edges,
);
collect_css_module_composes_edge_facts_in_range(
tokens,
open + 1,
close,
&branches,
effective_scope,
edges,
);
}
index = close + 1;
}
}
fn collect_immediate_css_module_composes_edge_facts(
tokens: &[Token<'_>],
start: usize,
end: usize,
owner_branches: &[SelectorBranch],
edges: &mut Vec<ParsedCssModuleComposesEdgeFact>,
) {
let owner_selector_names = sorted_selector_branch_names(owner_branches);
let mut index = start;
let mut block_depth = 0usize;
while index < end {
match tokens[index].kind {
SyntaxKind::LeftBrace | SyntaxKind::SassIndent => {
block_depth += 1;
index += 1;
continue;
}
SyntaxKind::RightBrace | SyntaxKind::SassDedent => {
block_depth = block_depth.saturating_sub(1);
index += 1;
continue;
}
_ => {}
}
if block_depth > 0
|| tokens[index].kind != SyntaxKind::Ident
|| !tokens[index].text.eq_ignore_ascii_case("composes")
{
index += 1;
continue;
}
let Some(colon_index) = next_non_trivia_token_index_until(tokens, index + 1, end) else {
index += 1;
continue;
};
if tokens[colon_index].kind != SyntaxKind::Colon {
index += 1;
continue;
}
let value_start = colon_index + 1;
let value_end = css_module_value_statement_end(tokens, value_start).min(end);
let from_index = top_level_token_text_index(tokens, value_start, value_end, "from");
let target_end = from_index.unwrap_or(value_end);
let target_names =
collect_css_module_composes_target_names(tokens, value_start, target_end);
if target_names.is_empty() {
index = value_end;
continue;
}
let (kind, import_source) = from_index
.and_then(|from_index| {
css_module_composes_import_edge_source(tokens, from_index + 1, value_end)
})
.map(|source| {
if source == "global" {
(ParsedCssModuleComposesEdgeKind::Global, Some(source))
} else {
(ParsedCssModuleComposesEdgeKind::External, Some(source))
}
})
.unwrap_or((ParsedCssModuleComposesEdgeKind::Local, None));
let range_end = value_end
.checked_sub(1)
.and_then(|end| tokens.get(end))
.map(|token| token.range.end())
.unwrap_or_else(|| tokens[index].range.end());
edges.push(ParsedCssModuleComposesEdgeFact {
kind,
owner_selector_names: owner_selector_names.clone(),
target_names,
import_source,
range: TextRange::new(tokens[index].range.start(), range_end),
});
index = value_end;
}
}
fn sorted_selector_branch_names(branches: &[SelectorBranch]) -> Vec<String> {
branches
.iter()
.map(|branch| branch.name.clone())
.collect::<BTreeSet<_>>()
.into_iter()
.collect()
}
fn collect_css_module_composes_target_names(
tokens: &[Token<'_>],
start: usize,
end: usize,
) -> Vec<String> {
let mut names = Vec::new();
let mut index = start;
while index < end {
if matches!(
tokens[index].kind,
SyntaxKind::Ident | SyntaxKind::CustomPropertyName
) && !tokens[index].text.eq_ignore_ascii_case("from")
&& !names.iter().any(|name| name == tokens[index].text)
{
names.push(tokens[index].text.to_string());
}
index += 1;
}
names
}
fn css_module_composes_import_edge_source(
tokens: &[Token<'_>],
start: usize,
end: usize,
) -> Option<String> {
let source_index = next_non_trivia_token_index_until(tokens, start, end)?;
let token = tokens[source_index];
matches!(
token.kind,
SyntaxKind::String | SyntaxKind::Url | SyntaxKind::Ident
)
.then(|| css_module_value_source_name(token))
}
fn collect_css_module_composes_targets(
tokens: &[Token<'_>],
start: usize,
end: usize,
composes: &mut Vec<ParsedCssModuleComposesFact>,
seen: &mut BTreeSet<(ParsedCssModuleComposesFactKind, String, u32, u32)>,
) {
let mut index = start;
while index < end {
if matches!(
tokens[index].kind,
SyntaxKind::Ident | SyntaxKind::CustomPropertyName
) && !tokens[index].text.eq_ignore_ascii_case("from")
{
push_css_module_composes_fact(
composes,
seen,
ParsedCssModuleComposesFactKind::Target,
tokens[index].text.to_string(),
tokens[index].range,
);
}
index += 1;
}
}
fn collect_css_module_composes_import_source(
tokens: &[Token<'_>],
start: usize,
end: usize,
composes: &mut Vec<ParsedCssModuleComposesFact>,
seen: &mut BTreeSet<(ParsedCssModuleComposesFactKind, String, u32, u32)>,
) {
if let Some(source_index) = next_non_trivia_token_index_until(tokens, start, end) {
let token = tokens[source_index];
if matches!(
token.kind,
SyntaxKind::String | SyntaxKind::Url | SyntaxKind::Ident
) {
push_css_module_composes_fact(
composes,
seen,
ParsedCssModuleComposesFactKind::ImportSource,
css_module_value_source_name(token),
token.range,
);
}
}
}
fn push_css_module_composes_fact(
composes: &mut Vec<ParsedCssModuleComposesFact>,
seen: &mut BTreeSet<(ParsedCssModuleComposesFactKind, String, u32, u32)>,
kind: ParsedCssModuleComposesFactKind,
name: String,
range: TextRange,
) {
if seen.insert((
kind,
name.clone(),
u32::from(range.start()),
u32::from(range.end()),
)) {
composes.push(ParsedCssModuleComposesFact { kind, name, range });
}
}
fn collect_icss_facts_from_tokens(tokens: &[Token<'_>]) -> Vec<ParsedIcssFact> {
let mut icss = Vec::new();
let mut seen = BTreeSet::new();
for (index, token) in tokens.iter().enumerate() {
if token.kind != SyntaxKind::Colon {
continue;
}
let Some(name_index) = next_non_trivia_token_index_until(tokens, index + 1, tokens.len())
else {
continue;
};
let name = tokens[name_index].text;
if !matches!(tokens[name_index].kind, SyntaxKind::Ident) {
continue;
}
if name.eq_ignore_ascii_case("export") {
if let Some((open, close)) =
find_block_after_header(tokens, name_index + 1, tokens.len())
{
collect_icss_export_names(tokens, open + 1, close, &mut icss, &mut seen);
}
continue;
}
if name.eq_ignore_ascii_case("import") {
collect_icss_import_source(tokens, name_index + 1, &mut icss, &mut seen);
if let Some((open, close)) =
find_block_after_header(tokens, name_index + 1, tokens.len())
{
collect_icss_import_names(tokens, open + 1, close, &mut icss, &mut seen);
}
}
}
icss
}
fn collect_icss_import_edge_facts_from_tokens(
tokens: &[Token<'_>],
) -> Vec<ParsedIcssImportEdgeFact> {
let mut edges = Vec::new();
for (index, token) in tokens.iter().enumerate() {
if token.kind != SyntaxKind::Colon {
continue;
}
let Some(name_index) = next_non_trivia_token_index_until(tokens, index + 1, tokens.len())
else {
continue;
};
if tokens[name_index].kind != SyntaxKind::Ident
|| !tokens[name_index].text.eq_ignore_ascii_case("import")
{
continue;
}
let Some(import_source) = icss_import_edge_source(tokens, name_index + 1) else {
continue;
};
if let Some((open, close)) = find_block_after_header(tokens, name_index + 1, tokens.len()) {
collect_icss_import_edges(tokens, open + 1, close, import_source, &mut edges);
}
}
edges
}
fn collect_icss_export_edge_facts_from_tokens(
tokens: &[Token<'_>],
) -> Vec<ParsedIcssExportEdgeFact> {
let mut edges = Vec::new();
for (index, token) in tokens.iter().enumerate() {
if token.kind != SyntaxKind::Colon {
continue;
}
let Some(name_index) = next_non_trivia_token_index_until(tokens, index + 1, tokens.len())
else {
continue;
};
if tokens[name_index].kind != SyntaxKind::Ident
|| !tokens[name_index].text.eq_ignore_ascii_case("export")
{
continue;
}
if let Some((open, close)) = find_block_after_header(tokens, name_index + 1, tokens.len()) {
collect_icss_export_edges(tokens, open + 1, close, &mut edges);
}
}
edges
}
fn collect_icss_export_edges(
tokens: &[Token<'_>],
start: usize,
end: usize,
edges: &mut Vec<ParsedIcssExportEdgeFact>,
) {
let mut index = start;
while index < end {
let token = tokens[index];
if matches!(
token.kind,
SyntaxKind::Ident | SyntaxKind::CustomPropertyName
) && let Some(colon_index) = next_non_trivia_token_index_until(tokens, index + 1, end)
&& tokens[colon_index].kind == SyntaxKind::Colon
{
let value_end = css_module_value_statement_end(tokens, colon_index + 1).min(end);
let reference_names = collect_css_module_value_definition_edge_names(
tokens,
colon_index + 1,
value_end,
css_module_value_reference_token_can_be_name,
);
if !reference_names.is_empty() {
let range_end = value_end
.checked_sub(1)
.and_then(|end| tokens.get(end))
.map(|token| token.range.end())
.unwrap_or_else(|| token.range.end());
edges.push(ParsedIcssExportEdgeFact {
export_name: token.text.to_string(),
reference_names,
range: TextRange::new(token.range.start(), range_end),
});
}
index = value_end;
continue;
}
index += 1;
}
}
fn icss_import_edge_source(tokens: &[Token<'_>], start: usize) -> Option<String> {
let open_index = next_non_trivia_token_index_until(tokens, start, tokens.len())?;
if tokens[open_index].kind != SyntaxKind::LeftParen {
return None;
}
let source_index = next_non_trivia_token_index_until(tokens, open_index + 1, tokens.len())?;
let token = tokens[source_index];
matches!(
token.kind,
SyntaxKind::String | SyntaxKind::Url | SyntaxKind::Ident
)
.then(|| css_module_value_source_name(token))
}
fn collect_icss_import_edges(
tokens: &[Token<'_>],
start: usize,
end: usize,
import_source: String,
edges: &mut Vec<ParsedIcssImportEdgeFact>,
) {
let mut index = start;
while index < end {
let token = tokens[index];
if matches!(
token.kind,
SyntaxKind::Ident | SyntaxKind::CustomPropertyName
) && let Some(colon_index) = next_non_trivia_token_index_until(tokens, index + 1, end)
&& tokens[colon_index].kind == SyntaxKind::Colon
&& let Some(remote_index) =
next_non_trivia_token_index_until(tokens, colon_index + 1, end)
&& matches!(
tokens[remote_index].kind,
SyntaxKind::Ident | SyntaxKind::CustomPropertyName
)
{
edges.push(ParsedIcssImportEdgeFact {
local_name: token.text.to_string(),
remote_name: tokens[remote_index].text.to_string(),
import_source: import_source.clone(),
range: token.range,
});
index = css_module_value_statement_end(tokens, colon_index + 1);
continue;
}
index += 1;
}
}
fn collect_icss_export_names(
tokens: &[Token<'_>],
start: usize,
end: usize,
icss: &mut Vec<ParsedIcssFact>,
seen: &mut BTreeSet<(ParsedIcssFactKind, String, u32, u32)>,
) {
let mut index = start;
while index < end {
let token = tokens[index];
if matches!(
token.kind,
SyntaxKind::Ident | SyntaxKind::CustomPropertyName
) && let Some(colon_index) = next_non_trivia_token_index_until(tokens, index + 1, end)
&& tokens[colon_index].kind == SyntaxKind::Colon
{
push_icss_fact(
icss,
seen,
ParsedIcssFactKind::ExportName,
token.text.to_string(),
token.range,
);
index = css_module_value_statement_end(tokens, colon_index + 1);
continue;
}
index += 1;
}
}
fn collect_icss_import_source(
tokens: &[Token<'_>],
start: usize,
icss: &mut Vec<ParsedIcssFact>,
seen: &mut BTreeSet<(ParsedIcssFactKind, String, u32, u32)>,
) {
let Some(open_index) = next_non_trivia_token_index_until(tokens, start, tokens.len()) else {
return;
};
if tokens[open_index].kind != SyntaxKind::LeftParen {
return;
}
let Some(source_index) =
next_non_trivia_token_index_until(tokens, open_index + 1, tokens.len())
else {
return;
};
let token = tokens[source_index];
if matches!(
token.kind,
SyntaxKind::String | SyntaxKind::Url | SyntaxKind::Ident
) {
push_icss_fact(
icss,
seen,
ParsedIcssFactKind::ImportSource,
css_module_value_source_name(token),
token.range,
);
}
}
fn collect_icss_import_names(
tokens: &[Token<'_>],
start: usize,
end: usize,
icss: &mut Vec<ParsedIcssFact>,
seen: &mut BTreeSet<(ParsedIcssFactKind, String, u32, u32)>,
) {
let mut index = start;
while index < end {
let token = tokens[index];
if matches!(
token.kind,
SyntaxKind::Ident | SyntaxKind::CustomPropertyName
) && let Some(colon_index) = next_non_trivia_token_index_until(tokens, index + 1, end)
&& tokens[colon_index].kind == SyntaxKind::Colon
{
push_icss_fact(
icss,
seen,
ParsedIcssFactKind::ImportLocalName,
token.text.to_string(),
token.range,
);
if let Some(remote_index) =
next_non_trivia_token_index_until(tokens, colon_index + 1, end)
&& matches!(
tokens[remote_index].kind,
SyntaxKind::Ident | SyntaxKind::CustomPropertyName
)
{
push_icss_fact(
icss,
seen,
ParsedIcssFactKind::ImportRemoteName,
tokens[remote_index].text.to_string(),
tokens[remote_index].range,
);
}
index = css_module_value_statement_end(tokens, colon_index + 1);
continue;
}
index += 1;
}
}
fn push_icss_fact(
icss: &mut Vec<ParsedIcssFact>,
seen: &mut BTreeSet<(ParsedIcssFactKind, String, u32, u32)>,
kind: ParsedIcssFactKind,
name: String,
range: TextRange,
) {
if seen.insert((
kind,
name.clone(),
u32::from(range.start()),
u32::from(range.end()),
)) {
icss.push(ParsedIcssFact { kind, name, range });
}
}
fn collect_animation_facts_from_tokens(tokens: &[Token<'_>]) -> Vec<ParsedAnimationFact> {
let mut animations = Vec::new();
let mut seen = BTreeSet::new();
for (index, token) in tokens.iter().enumerate() {
if token.kind == SyntaxKind::AtKeyword && token.text.eq_ignore_ascii_case("@keyframes") {
if let Some(name_index) =
next_non_trivia_token_index_until(tokens, index + 1, tokens.len())
&& let Some(name) = animation_name_from_token(tokens[name_index])
{
push_animation_fact(
&mut animations,
&mut seen,
ParsedAnimationFactKind::KeyframesDeclaration,
name,
tokens[name_index].range,
);
}
continue;
}
if token.kind == SyntaxKind::Ident
&& token.text.eq_ignore_ascii_case("animation-name")
&& let Some(colon_index) =
next_non_trivia_token_index_until(tokens, index + 1, tokens.len())
&& tokens[colon_index].kind == SyntaxKind::Colon
{
collect_animation_name_references_until(
tokens,
colon_index + 1,
&mut animations,
&mut seen,
);
}
if token.kind == SyntaxKind::Ident
&& token.text.eq_ignore_ascii_case("animation")
&& let Some(colon_index) =
next_non_trivia_token_index_until(tokens, index + 1, tokens.len())
&& tokens[colon_index].kind == SyntaxKind::Colon
{
collect_animation_shorthand_references_until(
tokens,
colon_index + 1,
&mut animations,
&mut seen,
);
}
}
animations
}
fn collect_animation_name_references_until(
tokens: &[Token<'_>],
start: usize,
animations: &mut Vec<ParsedAnimationFact>,
seen: &mut BTreeSet<(ParsedAnimationFactKind, String, u32, u32)>,
) {
let mut index = start;
let mut paren_depth = 0usize;
let mut bracket_depth = 0usize;
while index < tokens.len() {
match tokens[index].kind {
SyntaxKind::LeftParen => paren_depth += 1,
SyntaxKind::RightParen => paren_depth = paren_depth.saturating_sub(1),
SyntaxKind::LeftBracket => bracket_depth += 1,
SyntaxKind::RightBracket => bracket_depth = bracket_depth.saturating_sub(1),
SyntaxKind::Semicolon
| SyntaxKind::SassOptionalSemicolon
| SyntaxKind::RightBrace
| SyntaxKind::SassDedent
if paren_depth == 0 && bracket_depth == 0 =>
{
break;
}
_ => {}
}
if paren_depth == 0
&& bracket_depth == 0
&& let Some(name) = animation_name_from_token(tokens[index])
{
push_animation_fact(
animations,
seen,
ParsedAnimationFactKind::AnimationNameReference,
name,
tokens[index].range,
);
}
index += 1;
}
}
fn collect_animation_shorthand_references_until(
tokens: &[Token<'_>],
start: usize,
animations: &mut Vec<ParsedAnimationFact>,
seen: &mut BTreeSet<(ParsedAnimationFactKind, String, u32, u32)>,
) {
let mut index = start;
let mut paren_depth = 0usize;
let mut bracket_depth = 0usize;
while index < tokens.len() {
match tokens[index].kind {
SyntaxKind::LeftParen => paren_depth += 1,
SyntaxKind::RightParen => paren_depth = paren_depth.saturating_sub(1),
SyntaxKind::LeftBracket => bracket_depth += 1,
SyntaxKind::RightBracket => bracket_depth = bracket_depth.saturating_sub(1),
SyntaxKind::Semicolon
| SyntaxKind::SassOptionalSemicolon
| SyntaxKind::RightBrace
| SyntaxKind::SassDedent
if paren_depth == 0 && bracket_depth == 0 =>
{
break;
}
_ => {}
}
if paren_depth == 0
&& bracket_depth == 0
&& animation_shorthand_token_can_be_name(tokens, index)
&& let Some(name) = animation_name_from_token(tokens[index])
{
push_animation_fact(
animations,
seen,
ParsedAnimationFactKind::AnimationNameReference,
name,
tokens[index].range,
);
}
index += 1;
}
}
fn animation_shorthand_token_can_be_name(tokens: &[Token<'_>], index: usize) -> bool {
let token = tokens[index];
if token.kind == SyntaxKind::String {
return true;
}
if token.kind != SyntaxKind::Ident {
return false;
}
if let Some(next_index) = next_non_trivia_token_index_until(tokens, index + 1, tokens.len())
&& tokens[next_index].kind == SyntaxKind::LeftParen
{
return false;
}
!animation_shorthand_ident_is_non_name(token.text)
}
fn animation_shorthand_ident_is_non_name(name: &str) -> bool {
matches!(
name.to_ascii_lowercase().as_str(),
"ease"
| "ease-in"
| "ease-out"
| "ease-in-out"
| "linear"
| "step-start"
| "step-end"
| "infinite"
| "normal"
| "reverse"
| "alternate"
| "alternate-reverse"
| "running"
| "paused"
| "forwards"
| "backwards"
| "both"
| "replace"
| "add"
| "accumulate"
| "auto"
)
}
fn push_animation_fact(
animations: &mut Vec<ParsedAnimationFact>,
seen: &mut BTreeSet<(ParsedAnimationFactKind, String, u32, u32)>,
kind: ParsedAnimationFactKind,
name: String,
range: TextRange,
) {
if seen.insert((
kind,
name.clone(),
u32::from(range.start()),
u32::from(range.end()),
)) {
animations.push(ParsedAnimationFact { kind, name, range });
}
}
fn animation_name_from_token(token: Token<'_>) -> Option<String> {
if !matches!(token.kind, SyntaxKind::Ident | SyntaxKind::String) {
return None;
}
let name = token
.text
.trim_matches(|character| character == '"' || character == '\'')
.to_string();
if name.is_empty() || animation_name_is_reserved(&name) {
return None;
}
Some(name)
}
fn animation_name_is_reserved(name: &str) -> bool {
matches!(
name.to_ascii_lowercase().as_str(),
"none" | "initial" | "inherit" | "unset" | "revert" | "revert-layer"
)
}
fn containing_at_rule_header_name<'text>(
tokens: &'text [Token<'text>],
index: usize,
) -> Option<&'text str> {
let mut current = index;
while current > 0 {
current -= 1;
let token = tokens.get(current)?;
if token.kind.is_trivia() {
continue;
}
if matches!(
token.kind,
SyntaxKind::Semicolon
| SyntaxKind::SassOptionalSemicolon
| SyntaxKind::LeftBrace
| SyntaxKind::RightBrace
| SyntaxKind::SassIndent
| SyntaxKind::SassDedent
) {
return None;
}
if token.kind == SyntaxKind::AtKeyword {
return Some(token.text);
}
}
None
}
fn skip_trivia_tokens(tokens: &[Token<'_>], mut index: usize, end: usize) -> usize {
while index < end && tokens[index].kind.is_trivia() {
index += 1;
}
index
}
fn skip_statement(tokens: &[Token<'_>], mut index: usize, end: usize) -> usize {
while index < end {
match tokens[index].kind {
SyntaxKind::Semicolon | SyntaxKind::SassOptionalSemicolon => return index + 1,
SyntaxKind::RightBrace | SyntaxKind::SassDedent => return index,
_ => index += 1,
}
}
index
}
fn find_block_after_header(
tokens: &[Token<'_>],
start: usize,
end: usize,
) -> Option<(usize, usize)> {
let mut index = start;
while index < end {
match tokens[index].kind {
SyntaxKind::Semicolon
| SyntaxKind::SassOptionalSemicolon
| SyntaxKind::RightBrace
| SyntaxKind::SassDedent => return None,
SyntaxKind::LeftBrace => {
let close = matching_right_brace(tokens, index, end)?;
return Some((index, close));
}
SyntaxKind::SassIndent => {
let close = matching_sass_dedent(tokens, index, end)?;
return Some((index, close));
}
_ => index += 1,
}
}
None
}
fn matching_right_brace(tokens: &[Token<'_>], open: usize, end: usize) -> Option<usize> {
let mut depth = 0usize;
let mut index = open;
while index < end {
match tokens[index].kind {
SyntaxKind::LeftBrace => depth += 1,
SyntaxKind::RightBrace => {
depth = depth.saturating_sub(1);
if depth == 0 {
return Some(index);
}
}
_ => {}
}
index += 1;
}
None
}
fn matching_sass_dedent(tokens: &[Token<'_>], open: usize, end: usize) -> Option<usize> {
let mut depth = 0usize;
let mut index = open;
while index < end {
match tokens[index].kind {
SyntaxKind::SassIndent => depth += 1,
SyntaxKind::SassDedent => {
depth = depth.saturating_sub(1);
if depth == 0 {
return Some(index);
}
}
_ => {}
}
index += 1;
}
None
}
fn style_wrapper_at_rule(name: &str) -> bool {
matches_ignore_ascii_case(
name,
&[
"@media",
"@supports",
"@when",
"@else",
"@layer",
"@scope",
"@container",
"@starting-style",
"@if",
"@else",
"@for",
"@each",
"@while",
"@at-root",
"@include",
],
)
}
fn is_selector_combinator_kind(kind: SyntaxKind) -> bool {
matches!(
kind,
SyntaxKind::GreaterThan
| SyntaxKind::Plus
| SyntaxKind::Tilde
| SyntaxKind::ColumnCombinator
| SyntaxKind::DoublePipe
)
}
fn selector_component_can_start(kind: SyntaxKind) -> bool {
matches!(
kind,
SyntaxKind::Dot
| SyntaxKind::Hash
| SyntaxKind::Ident
| SyntaxKind::Star
| SyntaxKind::Ampersand
| SyntaxKind::ScssPlaceholder
| SyntaxKind::LeftBracket
| SyntaxKind::Colon
| SyntaxKind::DoubleColon
)
}
fn namespace_selector_target_can_start(kind: SyntaxKind) -> bool {
matches!(
kind,
SyntaxKind::Ident | SyntaxKind::CustomPropertyName | SyntaxKind::Star
)
}
fn keyframe_selector_token_is_valid(token: Token<'_>) -> bool {
token.kind == SyntaxKind::Percentage
|| (token.kind == SyntaxKind::Ident
&& (token.text.eq_ignore_ascii_case("from") || token.text.eq_ignore_ascii_case("to")))
}
fn selector_component_can_end(kind: SyntaxKind) -> bool {
matches!(
kind,
SyntaxKind::Ident
| SyntaxKind::CustomPropertyName
| SyntaxKind::Hash
| SyntaxKind::RightBracket
| SyntaxKind::RightParen
| SyntaxKind::Star
)
}
fn collect_at_rule_facts_from_tokens(
tokens: &[Token<'_>],
dialect: StyleDialect,
) -> Vec<ParsedAtRuleFact> {
tokens
.iter()
.filter(|token| token.kind == SyntaxKind::AtKeyword)
.map(|token| {
let css_spec = at_rule_spec(token.text);
let node_kind = css_spec
.or_else(|| match dialect {
StyleDialect::Scss | StyleDialect::Sass => scss_at_rule_spec(token.text),
StyleDialect::Css | StyleDialect::Less => None,
})
.map(|spec| spec.node_kind);
let name = if css_spec.is_some() {
token.text.to_ascii_lowercase()
} else {
token.text.to_string()
};
ParsedAtRuleFact {
name,
node_kind,
range: token.range,
}
})
.collect()
}
fn next_non_trivia_token<'text>(
tokens: &'text [Token<'text>],
mut index: usize,
) -> Option<Token<'text>> {
while let Some(token) = tokens.get(index).copied() {
if !token.kind.is_trivia() {
return Some(token);
}
index += 1;
}
None
}
fn next_non_trivia_token_until<'text>(
tokens: &'text [Token<'text>],
mut index: usize,
end: usize,
) -> Option<Token<'text>> {
while index < end {
let token = tokens.get(index).copied()?;
if !token.kind.is_trivia() {
return Some(token);
}
index += 1;
}
None
}
fn next_non_trivia_token_index_until(
tokens: &[Token<'_>],
mut index: usize,
end: usize,
) -> Option<usize> {
while index < end {
let token = tokens.get(index)?;
if !token.kind.is_trivia() {
return Some(index);
}
index += 1;
}
None
}
fn next_non_trivia_token_after_range<'text>(
tokens: &'text [Token<'text>],
range: TextRange,
end: usize,
) -> Option<Token<'text>> {
let index = token_index_by_range(tokens, range)?;
next_non_trivia_token_until(tokens, index + 1, end)
}
fn token_index_by_range(tokens: &[Token<'_>], range: TextRange) -> Option<usize> {
tokens.iter().position(|token| token.range == range)
}
fn matching_right_paren_from_range(
tokens: &[Token<'_>],
open_range: TextRange,
end: usize,
) -> Option<usize> {
let mut depth = 0usize;
let mut index = token_index_by_range(tokens, open_range)?;
while index < end {
match tokens[index].kind {
SyntaxKind::LeftParen => depth += 1,
SyntaxKind::RightParen => {
depth = depth.saturating_sub(1);
if depth == 0 {
return Some(index);
}
}
_ => {}
}
index += 1;
}
None
}
fn previous_non_trivia_token<'text>(
tokens: &'text [Token<'text>],
start: usize,
index: usize,
) -> Option<Token<'text>> {
let mut current = index;
while current > start {
current -= 1;
let token = tokens.get(current).copied()?;
if !token.kind.is_trivia() {
return Some(token);
}
}
None
}
fn at_rule_spec(text: &str) -> Option<AtRuleSpec> {
let lowered = text.to_ascii_lowercase();
let (node_kind, block_kind) = match lowered.as_str() {
"@media" => (SyntaxKind::MediaRule, AtRuleBlockKind::GroupRuleList),
"@supports" => (SyntaxKind::SupportsRule, AtRuleBlockKind::GroupRuleList),
"@when" => (SyntaxKind::WhenRule, AtRuleBlockKind::GroupRuleList),
"@else" => (SyntaxKind::ElseRule, AtRuleBlockKind::GroupRuleList),
"@container" => (SyntaxKind::ContainerRule, AtRuleBlockKind::GroupRuleList),
"@layer" => (SyntaxKind::LayerRule, AtRuleBlockKind::GroupRuleList),
"@scope" => (SyntaxKind::ScopeRule, AtRuleBlockKind::GroupRuleList),
"@starting-style" => (
SyntaxKind::StartingStyleRule,
AtRuleBlockKind::GroupRuleList,
),
"@nest" => (SyntaxKind::NestRule, AtRuleBlockKind::DeclarationList),
"@keyframes" => (SyntaxKind::KeyframesRule, AtRuleBlockKind::Keyframes),
"@font-face" => (SyntaxKind::FontFaceRule, AtRuleBlockKind::DeclarationList),
"@page" => (SyntaxKind::PageRule, AtRuleBlockKind::DeclarationList),
"@property" => (SyntaxKind::PropertyRule, AtRuleBlockKind::DeclarationList),
"@counter-style" => (
SyntaxKind::CounterStyleRule,
AtRuleBlockKind::DeclarationList,
),
"@font-palette-values" => (
SyntaxKind::FontPaletteValuesRule,
AtRuleBlockKind::DeclarationList,
),
"@color-profile" => (
SyntaxKind::ColorProfileRule,
AtRuleBlockKind::DeclarationList,
),
"@position-try" => (
SyntaxKind::PositionTryRule,
AtRuleBlockKind::DeclarationList,
),
"@font-feature-values" => (
SyntaxKind::FontFeatureValuesRule,
AtRuleBlockKind::GroupRuleList,
),
"@stylistic" => (
SyntaxKind::FontFeatureValuesStylisticRule,
AtRuleBlockKind::DeclarationList,
),
"@styleset" => (
SyntaxKind::FontFeatureValuesStylesetRule,
AtRuleBlockKind::DeclarationList,
),
"@character-variant" => (
SyntaxKind::FontFeatureValuesCharacterVariantRule,
AtRuleBlockKind::DeclarationList,
),
"@swash" => (
SyntaxKind::FontFeatureValuesSwashRule,
AtRuleBlockKind::DeclarationList,
),
"@ornaments" => (
SyntaxKind::FontFeatureValuesOrnamentsRule,
AtRuleBlockKind::DeclarationList,
),
"@annotation" => (
SyntaxKind::FontFeatureValuesAnnotationRule,
AtRuleBlockKind::DeclarationList,
),
"@historical-forms" => (
SyntaxKind::FontFeatureValuesHistoricalFormsRule,
AtRuleBlockKind::DeclarationList,
),
"@view-transition" => (
SyntaxKind::ViewTransitionRule,
AtRuleBlockKind::DeclarationList,
),
"@charset" => (SyntaxKind::CharsetRule, AtRuleBlockKind::Raw),
"@import" => (SyntaxKind::ImportRule, AtRuleBlockKind::Raw),
"@namespace" => (SyntaxKind::NamespaceRule, AtRuleBlockKind::Raw),
"@custom-media" => (SyntaxKind::CustomMediaRule, AtRuleBlockKind::Raw),
text if is_page_margin_at_rule(text) => {
(SyntaxKind::PageMarginRule, AtRuleBlockKind::DeclarationList)
}
_ => return None,
};
Some(AtRuleSpec {
node_kind,
block_kind,
})
}
fn is_page_margin_at_rule(text: &str) -> bool {
matches!(
text,
"@top-left-corner"
| "@top-left"
| "@top-center"
| "@top-right"
| "@top-right-corner"
| "@bottom-left-corner"
| "@bottom-left"
| "@bottom-center"
| "@bottom-right"
| "@bottom-right-corner"
| "@left-top"
| "@left-middle"
| "@left-bottom"
| "@right-top"
| "@right-middle"
| "@right-bottom"
)
}
fn scss_at_rule_spec(text: &str) -> Option<AtRuleSpec> {
let (node_kind, block_kind) = match text {
"@use" => (SyntaxKind::ScssUseRule, AtRuleBlockKind::Raw),
"@forward" => (SyntaxKind::ScssForwardRule, AtRuleBlockKind::Raw),
"@mixin" => (
SyntaxKind::ScssMixinDeclaration,
AtRuleBlockKind::DeclarationList,
),
"@include" => (
SyntaxKind::ScssIncludeRule,
AtRuleBlockKind::DeclarationList,
),
"@function" => (
SyntaxKind::ScssFunctionDeclaration,
AtRuleBlockKind::DeclarationList,
),
"@return" => (SyntaxKind::ScssReturnRule, AtRuleBlockKind::Raw),
"@extend" => (SyntaxKind::ScssExtendRule, AtRuleBlockKind::Raw),
"@if" => (SyntaxKind::ScssControlIf, AtRuleBlockKind::DeclarationList),
"@else" => (
SyntaxKind::ScssControlElse,
AtRuleBlockKind::DeclarationList,
),
"@each" => (
SyntaxKind::ScssControlEach,
AtRuleBlockKind::DeclarationList,
),
"@for" => (SyntaxKind::ScssControlFor, AtRuleBlockKind::DeclarationList),
"@while" => (
SyntaxKind::ScssControlWhile,
AtRuleBlockKind::DeclarationList,
),
"@at-root" => (SyntaxKind::ScssAtRootRule, AtRuleBlockKind::DeclarationList),
"@error" => (SyntaxKind::ScssErrorRule, AtRuleBlockKind::Raw),
"@warn" => (SyntaxKind::ScssWarnRule, AtRuleBlockKind::Raw),
"@debug" => (SyntaxKind::ScssDebugRule, AtRuleBlockKind::Raw),
"@content" => (SyntaxKind::ScssContentRule, AtRuleBlockKind::Raw),
_ => return None,
};
Some(AtRuleSpec {
node_kind,
block_kind,
})
}
fn is_selector_boundary(kind: SyntaxKind) -> bool {
matches!(
kind,
SyntaxKind::Comma
| SyntaxKind::LeftBrace
| SyntaxKind::SassIndent
| SyntaxKind::RightBrace
| SyntaxKind::SassDedent
| SyntaxKind::Semicolon
| SyntaxKind::SassOptionalSemicolon
)
}
fn is_selector_boundary_until(kind: SyntaxKind, recovery: &[SyntaxKind]) -> bool {
is_selector_boundary(kind) || recovery.contains(&kind)
}
fn is_selector_list_pseudo_class(text: &str) -> bool {
matches!(text, "is" | "where" | "local" | "global")
}
fn is_nth_pseudo_class(text: &str) -> bool {
matches!(
text,
"nth-child" | "nth-last-child" | "nth-of-type" | "nth-last-of-type"
)
}
fn language_tag_token_can_start(kind: SyntaxKind) -> bool {
matches!(kind, SyntaxKind::Ident | SyntaxKind::String)
}
fn selector_item_token_is_recoverable(kind: SyntaxKind) -> bool {
matches!(
kind,
SyntaxKind::Whitespace
| SyntaxKind::SassIndentedNewline
| SyntaxKind::Dot
| SyntaxKind::Comma
| SyntaxKind::Hash
| SyntaxKind::Ident
| SyntaxKind::CustomPropertyName
| SyntaxKind::String
| SyntaxKind::Number
| SyntaxKind::Percentage
| SyntaxKind::Dimension
| SyntaxKind::Star
| SyntaxKind::Ampersand
| SyntaxKind::ScssPlaceholder
| SyntaxKind::LeftBracket
| SyntaxKind::RightBracket
| SyntaxKind::Colon
| SyntaxKind::DoubleColon
| SyntaxKind::LeftParen
| SyntaxKind::RightParen
| SyntaxKind::Equals
| SyntaxKind::IncludesMatch
| SyntaxKind::DashMatch
| SyntaxKind::PrefixMatch
| SyntaxKind::SuffixMatch
| SyntaxKind::SubstringMatch
| SyntaxKind::Pipe
| SyntaxKind::ColumnCombinator
| SyntaxKind::GreaterThan
| SyntaxKind::Plus
| SyntaxKind::Minus
| SyntaxKind::Tilde
| SyntaxKind::KeywordAnd
| SyntaxKind::KeywordOr
| SyntaxKind::KeywordNot
)
}
fn is_at_rule_prelude_boundary(kind: SyntaxKind) -> bool {
matches!(
kind,
SyntaxKind::LeftBrace
| SyntaxKind::SassIndent
| SyntaxKind::Semicolon
| SyntaxKind::SassOptionalSemicolon
)
}
fn is_statement_end(kind: SyntaxKind) -> bool {
matches!(
kind,
SyntaxKind::Semicolon | SyntaxKind::SassOptionalSemicolon
)
}
fn sass_token_can_end_statement(kind: SyntaxKind) -> bool {
!matches!(
kind,
SyntaxKind::Whitespace
| SyntaxKind::LineComment
| SyntaxKind::BlockComment
| SyntaxKind::SassIndentedNewline
| SyntaxKind::SassIndent
| SyntaxKind::SassDedent
| SyntaxKind::SassOptionalSemicolon
| SyntaxKind::Comma
| SyntaxKind::Colon
| SyntaxKind::DoubleColon
| SyntaxKind::LeftBrace
| SyntaxKind::LeftParen
| SyntaxKind::LeftBracket
| SyntaxKind::Plus
| SyntaxKind::Minus
| SyntaxKind::Star
| SyntaxKind::Slash
| SyntaxKind::GreaterThan
| SyntaxKind::LessThan
| SyntaxKind::Equals
| SyntaxKind::Arrow
| SyntaxKind::Pipe
| SyntaxKind::Tilde
| SyntaxKind::Caret
| SyntaxKind::Ampersand
| SyntaxKind::DoubleAmpersand
| SyntaxKind::ColumnCombinator
| SyntaxKind::IncludesMatch
| SyntaxKind::DashMatch
| SyntaxKind::PrefixMatch
| SyntaxKind::SuffixMatch
| SyntaxKind::SubstringMatch
| SyntaxKind::PlusEquals
| SyntaxKind::MinusEquals
| SyntaxKind::SlashEquals
)
}
fn function_argument_recovery(recovery: &[SyntaxKind]) -> Vec<SyntaxKind> {
let mut kinds = vec![SyntaxKind::RightParen];
for kind in recovery {
if !kinds.contains(kind) {
kinds.push(*kind);
}
}
kinds
}
fn bracketed_value_recovery(recovery: &[SyntaxKind]) -> Vec<SyntaxKind> {
let mut kinds = vec![SyntaxKind::RightBracket];
for kind in recovery {
if !kinds.contains(kind) {
kinds.push(*kind);
}
}
kinds
}
fn simple_block_recovery(close_kind: SyntaxKind, recovery: &[SyntaxKind]) -> Vec<SyntaxKind> {
let mut kinds = vec![close_kind];
for kind in recovery {
if !kinds.contains(kind) {
kinds.push(*kind);
}
}
kinds
}
fn matching_simple_block_close(open_kind: SyntaxKind) -> Option<SyntaxKind> {
match open_kind {
SyntaxKind::LeftBrace => Some(SyntaxKind::RightBrace),
SyntaxKind::LeftBracket => Some(SyntaxKind::RightBracket),
SyntaxKind::LeftParen => Some(SyntaxKind::RightParen),
_ => None,
}
}
fn value_list_item_recovery(recovery: &[SyntaxKind]) -> Vec<SyntaxKind> {
let mut kinds = vec![SyntaxKind::Comma];
for kind in recovery {
if !kinds.contains(kind) {
kinds.push(*kind);
}
}
kinds
}
fn comma_separated_component_value_list_item_recovery(recovery: &[SyntaxKind]) -> Vec<SyntaxKind> {
let mut kinds = vec![SyntaxKind::Comma];
for kind in recovery {
if !kinds.contains(kind) {
kinds.push(*kind);
}
}
kinds
}
fn variable_declaration_node_kind(kind: SyntaxKind, has_colon: bool) -> SyntaxKind {
if has_colon {
return kind;
}
match kind {
SyntaxKind::ScssVariableDeclaration => SyntaxKind::BogusScssVariable,
SyntaxKind::LessVariableDeclaration => SyntaxKind::BogusLessVariable,
_ => kind,
}
}
fn is_attribute_matcher(kind: SyntaxKind) -> bool {
matches!(
kind,
SyntaxKind::Equals
| SyntaxKind::IncludesMatch
| SyntaxKind::DashMatch
| SyntaxKind::PrefixMatch
| SyntaxKind::SuffixMatch
| SyntaxKind::SubstringMatch
)
}
fn attribute_name_token_can_start(kind: SyntaxKind) -> bool {
matches!(
kind,
SyntaxKind::Ident | SyntaxKind::CustomPropertyName | SyntaxKind::Star
)
}
fn attribute_name_token_can_continue(kind: SyntaxKind) -> bool {
matches!(
kind,
SyntaxKind::Ident
| SyntaxKind::CustomPropertyName
| SyntaxKind::Star
| SyntaxKind::Pipe
| SyntaxKind::ColumnCombinator
)
}
fn attribute_value_token_can_start(kind: SyntaxKind) -> bool {
matches!(
kind,
SyntaxKind::Ident
| SyntaxKind::CustomPropertyName
| SyntaxKind::String
| SyntaxKind::Hash
| SyntaxKind::Number
| SyntaxKind::Dimension
)
}
fn is_combinator(kind: SyntaxKind) -> bool {
matches!(
kind,
SyntaxKind::GreaterThan
| SyntaxKind::Plus
| SyntaxKind::Tilde
| SyntaxKind::ColumnCombinator
)
}
fn infix_binding_power(kind: SyntaxKind) -> Option<(u8, u8)> {
match kind {
SyntaxKind::Plus | SyntaxKind::Minus => Some((1, 2)),
SyntaxKind::Star | SyntaxKind::Slash | SyntaxKind::Percent => Some((3, 4)),
_ => None,
}
}
fn specialized_function_kind(text: &str) -> Option<SyntaxKind> {
if text.eq_ignore_ascii_case("var") {
return Some(SyntaxKind::VarFunction);
}
if text.eq_ignore_ascii_case("calc") {
return Some(SyntaxKind::CalcFunction);
}
if text.eq_ignore_ascii_case("env") {
return Some(SyntaxKind::EnvFunction);
}
if text.eq_ignore_ascii_case("attr") {
return Some(SyntaxKind::AttrFunction);
}
if matches_ignore_ascii_case(text, VALUES_L4_MATH_FUNCTION_NAMES) {
return Some(SyntaxKind::MathFunction);
}
if matches_ignore_ascii_case(text, CSS_COLOR_FUNCTION_NAMES) {
return Some(SyntaxKind::ColorValue);
}
if matches_ignore_ascii_case(text, CSS_GRADIENT_FUNCTION_NAMES) {
return Some(SyntaxKind::GradientFunction);
}
if matches_ignore_ascii_case(text, CSS_TRANSFORM_FUNCTION_NAMES) {
return Some(SyntaxKind::TransformFunction);
}
if matches_ignore_ascii_case(text, CSS_FILTER_FUNCTION_NAMES) {
return Some(SyntaxKind::FilterFunction);
}
if matches_ignore_ascii_case(text, CSS_IMAGE_FUNCTION_NAMES) {
return Some(SyntaxKind::ImageFunction);
}
if matches_ignore_ascii_case(text, CSS_SHAPE_FUNCTION_NAMES) {
return Some(SyntaxKind::ShapeFunction);
}
None
}
fn function_argument_count_is_valid(function_name: &str, argument_count: usize) -> bool {
if function_name.eq_ignore_ascii_case("calc") {
return argument_count == 1;
}
if matches_ignore_ascii_case(function_name, &["min", "max", "hypot"]) {
return argument_count >= 1;
}
if function_name.eq_ignore_ascii_case("clamp") {
return argument_count == 3;
}
if function_name.eq_ignore_ascii_case("round") {
return (2..=3).contains(&argument_count);
}
if function_name.eq_ignore_ascii_case("log") {
return (1..=2).contains(&argument_count);
}
if matches_ignore_ascii_case(function_name, &["mod", "rem", "pow", "atan2"]) {
return argument_count == 2;
}
if matches_ignore_ascii_case(
function_name,
&[
"sin", "cos", "tan", "asin", "acos", "atan", "sqrt", "exp", "abs", "sign",
],
) {
return argument_count == 1;
}
if function_name.eq_ignore_ascii_case("color-mix") {
return argument_count == 3;
}
if function_name.eq_ignore_ascii_case("light-dark") {
return argument_count == 2;
}
if function_name.eq_ignore_ascii_case("contrast-color") {
return argument_count == 1;
}
true
}
fn function_requires_filled_top_level_arguments(function_name: &str) -> bool {
function_name.eq_ignore_ascii_case("calc")
|| matches_ignore_ascii_case(function_name, VALUES_L4_MATH_FUNCTION_NAMES)
|| matches_ignore_ascii_case(
function_name,
&["color-mix", "light-dark", "contrast-color"],
)
}
fn at_rule_prelude_head_is_custom_property_name(kind: SyntaxKind) -> bool {
kind == SyntaxKind::CustomPropertyName || is_interpolation_start(kind)
}
fn at_rule_prelude_head_is_custom_ident(kind: SyntaxKind) -> bool {
kind == SyntaxKind::Ident || is_interpolation_start(kind)
}
fn is_dynamic_function_argument_head(kind: SyntaxKind) -> bool {
matches!(
kind,
SyntaxKind::ScssVariable
| SyntaxKind::LessVariable
| SyntaxKind::ScssInterpolationStart
| SyntaxKind::LessInterpolationStart
)
}
fn is_scss_module_source_token(kind: SyntaxKind) -> bool {
matches!(
kind,
SyntaxKind::String | SyntaxKind::Url | SyntaxKind::ScssInterpolationStart
)
}
fn is_scss_module_namespace_token(kind: SyntaxKind) -> bool {
matches!(
kind,
SyntaxKind::Ident | SyntaxKind::Star | SyntaxKind::ScssInterpolationStart
)
}
fn is_scss_module_visibility_name_token(kind: SyntaxKind) -> bool {
matches!(
kind,
SyntaxKind::Ident
| SyntaxKind::ScssVariable
| SyntaxKind::ScssPlaceholder
| SyntaxKind::ScssInterpolationStart
)
}
fn is_css_module_from_source_token(kind: SyntaxKind, text: &str) -> bool {
matches!(
kind,
SyntaxKind::String
| SyntaxKind::Url
| SyntaxKind::ScssInterpolationStart
| SyntaxKind::LessInterpolationStart
) || (kind == SyntaxKind::Ident && text == "global")
}
fn is_scss_control_rule_kind(kind: SyntaxKind) -> bool {
matches!(
kind,
SyntaxKind::ScssControlIf
| SyntaxKind::ScssControlElse
| SyntaxKind::ScssControlEach
| SyntaxKind::ScssControlFor
| SyntaxKind::ScssControlWhile
)
}
fn matches_ignore_ascii_case(value: &str, candidates: &[&str]) -> bool {
candidates
.iter()
.any(|candidate| value.eq_ignore_ascii_case(candidate))
}
fn css_module_scope_function_kind(text: &str) -> Option<SyntaxKind> {
match text {
"local" => Some(SyntaxKind::CssModuleLocalBlock),
"global" => Some(SyntaxKind::CssModuleGlobalBlock),
_ => None,
}
}
fn text_range(start: usize, end: usize) -> TextRange {
TextRange::new(TextSize::from(start as u32), TextSize::from(end as u32))
}
#[cfg(test)]
mod tests;