use std::collections::BTreeSet;
use omena_cascade::{CascadeOriginV0, LayerOrdinal};
use omena_parser::{ParserByteSpanV0, ParserDeclarationSyntaxFactV0, StyleDialect};
use omena_query_checker_orchestrator::{CanonicalSelector, OmenaCheckerCascadeDeclarationInputV0};
use omena_query_transform_runner::expand_css_nested_selector;
use omena_semantic::{
LayerBindingResolutionV0, StyleContextIndexV0,
collect_parser_declaration_syntax_and_style_context_from_source, layer_ordinal_for_byte_span,
};
use omena_syntax::ident::ClassNameV0;
use super::runtime_state::query_selector_class_names;
use super::value_references::collect_query_var_references_in_value;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(in crate::style) struct ParsedDeclarationFactV0 {
pub(in crate::style) declaration_id: String,
pub(in crate::style) byte_span: ParserByteSpanV0,
pub(in crate::style) selector: String,
pub(in crate::style) property_name: String,
pub(in crate::style) value: String,
pub(in crate::style) important: bool,
pub(in crate::style) condition_context: Vec<String>,
pub(in crate::style) semantic_context_ids: Vec<String>,
pub(in crate::style) layer_name: Option<String>,
pub(in crate::style) layer_order: Option<i32>,
pub(in crate::style) source_order: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(in crate::style) struct ParsedDeclarationFactCollectionV0 {
pub(in crate::style) facts: Vec<ParsedDeclarationFactV0>,
pub(in crate::style) topology_incomplete_unresolved_count: Option<usize>,
}
impl ParsedDeclarationFactV0 {
#[allow(dead_code)]
pub(in crate::style) fn checker_input(&self) -> OmenaCheckerCascadeDeclarationInputV0 {
OmenaCheckerCascadeDeclarationInputV0 {
declaration_id: self.declaration_id.clone(),
selector: CanonicalSelector::from_canonical(self.selector.as_str()),
property: self.property_name.clone(),
value: self.value.clone(),
source_order: self.source_order,
condition_context: self.condition_context.clone(),
layer_name: self.layer_name.clone(),
layer_order: self.layer_order,
origin: CascadeOriginV0::Author,
important: self.important,
var_references: collect_query_var_references_in_value(self.value.as_str()),
}
}
}
#[allow(dead_code)]
pub(in crate::style) fn collect_parsed_declaration_facts(
style_path: &str,
source: &str,
dialect: StyleDialect,
) -> Vec<ParsedDeclarationFactV0> {
collect_parsed_declaration_fact_collection(style_path, source, dialect).facts
}
pub(in crate::style) fn collect_parsed_declaration_fact_collection(
_style_path: &str,
source: &str,
dialect: StyleDialect,
) -> ParsedDeclarationFactCollectionV0 {
let (syntax_facts, context_index) =
collect_parser_declaration_syntax_and_style_context_from_source(source, dialect);
collect_parsed_declaration_fact_collection_from_syntax_and_context(syntax_facts, &context_index)
}
pub(in crate::style) fn collect_parsed_declaration_fact_collection_from_syntax_and_context(
syntax_facts: Vec<ParserDeclarationSyntaxFactV0>,
context_index: &StyleContextIndexV0,
) -> ParsedDeclarationFactCollectionV0 {
let topology_incomplete_unresolved_count = (!context_index.layer_index.topology_complete)
.then_some(context_index.layer_index.unresolved_topology_count);
let facts = join_declaration_syntax_and_context(syntax_facts, context_index);
ParsedDeclarationFactCollectionV0 {
facts,
topology_incomplete_unresolved_count,
}
}
fn join_declaration_syntax_and_context(
syntax_facts: Vec<ParserDeclarationSyntaxFactV0>,
context_index: &StyleContextIndexV0,
) -> Vec<ParsedDeclarationFactV0> {
let mut facts = Vec::new();
for syntax_fact in syntax_facts {
let selectors = canonical_selectors_for_syntax_fact(&syntax_fact);
for selector in selectors {
let value = syntax_fact.value_text.clone();
let source_order = facts.len().min(u32::MAX as usize) as u32;
let (layer_name, layer_order) =
layer_binding_for_span(context_index, syntax_fact.byte_span);
facts.push(ParsedDeclarationFactV0 {
declaration_id: declaration_id_for_source_order(source_order),
byte_span: syntax_fact.byte_span,
semantic_context_ids: semantic_context_ids_for_selector(
context_index,
selector.as_str(),
),
selector,
property_name: syntax_fact.property_name.clone(),
value,
important: syntax_fact.important,
condition_context: syntax_fact.condition_contexts.clone(),
layer_name,
layer_order,
source_order,
});
}
}
facts
}
fn canonical_selectors_for_syntax_fact(fact: &ParserDeclarationSyntaxFactV0) -> Vec<String> {
let mut current = Vec::<String>::new();
for context in &fact.selector_contexts {
let parents = if context.reset_to_root {
Vec::new()
} else {
current.clone()
};
let mut next = Vec::new();
for member in &context.selector_members {
if parents.is_empty() {
push_unique(&mut next, member.trim().to_string());
continue;
}
for parent in &parents {
let expanded = expand_css_nested_selector(parent, member).unwrap_or_else(|| {
if member.contains('&') {
member.replace('&', parent)
} else {
format!("{parent} {member}")
}
});
push_unique(&mut next, expanded);
}
}
current = next;
}
current
}
fn push_unique(values: &mut Vec<String>, value: String) {
if !value.is_empty() && !values.contains(&value) {
values.push(value);
}
}
fn semantic_context_ids_for_selector(
context_index: &StyleContextIndexV0,
selector: &str,
) -> Vec<String> {
let class_names = query_selector_class_names(selector)
.into_iter()
.map(ClassNameV0::new)
.collect::<Vec<_>>();
let mut ids = BTreeSet::new();
for membership in context_index
.layer_index
.selector_memberships
.iter()
.chain(&context_index.container_index.selector_memberships)
.chain(&context_index.scope_index.selector_memberships)
{
let membership_name = ClassNameV0::new(membership.selector_name.as_str());
if class_names
.iter()
.any(|class_name| class_name.same_as(&membership_name))
{
ids.insert(membership.context_id.clone());
}
}
ids.into_iter().collect()
}
fn layer_binding_for_span(
context_index: &StyleContextIndexV0,
span: ParserByteSpanV0,
) -> (Option<String>, Option<i32>) {
let layer_index = &context_index.layer_index;
let resolution = layer_ordinal_for_byte_span(layer_index, span.start, span.end);
let binding = layer_index
.block_bindings
.iter()
.filter(|binding| {
binding.byte_span.start <= span.start && span.end <= binding.byte_span.end
})
.max_by_key(|binding| binding.nesting_depth);
match resolution {
LayerBindingResolutionV0::Resolved(ordinal) => (
binding.map(|binding| binding.canonical_name.clone()),
ordinal.map(LayerOrdinal::get),
),
LayerBindingResolutionV0::TopologyIncomplete { .. } => (None, None),
}
}
fn declaration_id_for_source_order(source_order: u32) -> String {
format!("decl-{source_order}")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cst_join_keeps_live_url_values_and_wire_ids() {
for source in [
".a { background-image: url(a;b.png)!important; background-image: url(clean.png); }",
".a { background-image: url(a}b.png)!important; background-image: url(clean.png); }",
] {
let facts = collect_parsed_declaration_facts("fixture.css", source, StyleDialect::Css);
assert_eq!(facts.len(), 2, "{facts:#?}");
assert_eq!(facts[0].declaration_id, "decl-0");
assert_eq!(facts[1].declaration_id, "decl-1");
assert!(facts[0].important);
assert!(facts[0].value.starts_with("url(a"));
assert!(facts[0].value.ends_with("b.png)"));
assert_eq!(facts[1].value, "url(clean.png)");
}
}
#[test]
fn cst_join_preserves_ids_above_a_trailing_edit() {
let before = collect_parsed_declaration_facts(
"fixture.css",
".a { color: red; color: blue; }",
StyleDialect::Css,
);
let after = collect_parsed_declaration_facts(
"fixture.css",
".a { color: red; color: blue; } .later { display: block; }",
StyleDialect::Css,
);
assert_eq!(
before
.iter()
.map(|fact| fact.declaration_id.as_str())
.collect::<Vec<_>>(),
["decl-0", "decl-1"]
);
assert_eq!(before, after[..before.len()]);
}
#[test]
fn cst_join_reparse_preserves_declaration_ids_and_spans() {
let source = r#"
/* header */
@layer theme {
.target { background-image: url(a;b.png)!important; }
.target { background-image: url(clean.png); }
}
"#;
let first = collect_parsed_declaration_facts("fixture.scss", source, StyleDialect::Scss);
let second = collect_parsed_declaration_facts("fixture.scss", source, StyleDialect::Scss);
let identity_projection = |facts: &[ParsedDeclarationFactV0]| {
facts
.iter()
.map(|fact| {
(
fact.declaration_id.clone(),
fact.source_order,
fact.byte_span,
)
})
.collect::<Vec<_>>()
};
assert_eq!(first, second);
assert_eq!(
identity_projection(&first),
identity_projection(&second),
"reparsing identical bytes must preserve the declaration wire IDs and their CST spans",
);
assert_eq!(
identity_projection(&first)
.iter()
.map(|(id, _, _)| id.as_str())
.collect::<Vec<_>>(),
["decl-0", "decl-1"],
);
}
#[test]
fn cst_join_materializes_parser_once_for_syntax_and_context() {
let (_, instrumentation) = omena_parser::with_omena_parser_parse_instrumentation(|| {
collect_parsed_declaration_facts(
"fixture.scss",
"@layer theme { .card { color: red; } }",
StyleDialect::Scss,
)
});
assert_eq!(instrumentation.parse_invocation_count, 1);
}
#[test]
fn cst_join_carries_semantic_context_memberships_and_nested_selectors() {
let source = r#"
@layer theme {
@container card (inline-size > 20rem) {
.card { &:hover { color: red; } }
}
}
"#;
let facts = collect_parsed_declaration_facts("fixture.scss", source, StyleDialect::Scss);
assert_eq!(facts.len(), 1, "{facts:#?}");
assert_eq!(facts[0].selector, ".card:hover");
assert_eq!(facts[0].layer_name.as_deref(), Some("theme"));
assert!(facts[0].layer_order.is_some());
assert!(
facts[0]
.semantic_context_ids
.iter()
.any(|id| id.starts_with("layer:")),
"{:?}",
facts[0].semantic_context_ids
);
assert!(
facts[0]
.semantic_context_ids
.iter()
.any(|id| id.starts_with("container:")),
"{:?}",
facts[0].semantic_context_ids
);
}
#[test]
fn declaration_id_derivation_is_the_existing_wire_spelling() {
assert_eq!(declaration_id_for_source_order(0), "decl-0");
assert_eq!(declaration_id_for_source_order(41), "decl-41");
}
}