1use std::collections::BTreeMap;
2
3use omena_parser::{
4 ModuleInstanceKeyV0, ParsedAnimationFactKind, ParsedEmissionSelectorFactKindV0,
5 ParsedEmissionSelectorFactsV0, ParsedSelectorFactKind, ParsedStyleFacts,
6};
7use serde::Serialize;
8
9use crate::{
10 GlobalRuleOrderV0, LinkedEmissionMaterializationErrorV0, LinkedStylesheetRuleV0,
11 TransformBundleLinkErrorV0, emission_order::EmissionModulePlanV0,
12};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
15#[serde(tag = "category", content = "nodeKind", rename_all = "camelCase")]
16#[non_exhaustive]
17pub enum EmissionItemKindV0 {
18 SelectorClass,
19 SelectorId,
20 SelectorPlaceholder,
21 SelectorElement,
22 SelectorAttribute,
23 SelectorUniversal,
24 SelectorPseudoClass,
25 SelectorPseudoElement,
26 UnknownStructuralSelector,
27 AtRule { node_kind: u32 },
28 UnknownAtRule,
29 KeyframesDeclaration,
30 AnimationNameReference,
31 ModuleBoundary,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
35#[serde(rename_all = "camelCase")]
36#[non_exhaustive]
37pub struct EmissionItemV0 {
38 pub kind: EmissionItemKindV0,
39 pub name: String,
40 pub range_start: u32,
41 pub range_end: u32,
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
45#[serde(rename_all = "camelCase")]
46#[non_exhaustive]
47pub enum EmissionItemFactCategoryV0 {
48 NamedSelectors,
49 EmissionSelectors,
50 Variables,
51 SassSymbols,
52 SassIncludes,
53 SassModuleEdges,
54 SassPlaceholderDefinitions,
55 ExtendTargets,
56 Animations,
57 CssModuleValues,
58 CssModuleValueImportEdges,
59 CssModuleValueDefinitionEdges,
60 CssModuleComposes,
61 CssModuleComposesEdges,
62 Icss,
63 IcssImportEdges,
64 IcssExportEdges,
65 AtRules,
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
69#[serde(rename_all = "camelCase")]
70#[non_exhaustive]
71pub enum EmissionItemProjectionDispositionV0 {
72 Projected,
73 NotProjected,
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
77#[serde(rename_all = "camelCase")]
78#[non_exhaustive]
79pub enum EmissionItemProjectionReasonV0 {
80 SourceOrderedNamedSelector,
81 SourceOrderedStructuralSelector,
82 SourceOrderedAtRule,
83 SourceOrderedAnimation,
84 NotAnEmissionBoundary,
85}
86
87#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
88#[serde(rename_all = "camelCase")]
89#[non_exhaustive]
90pub struct EmissionItemProjectionDisclosureV0 {
91 pub category: EmissionItemFactCategoryV0,
92 pub disposition: EmissionItemProjectionDispositionV0,
93 pub reason: EmissionItemProjectionReasonV0,
94}
95
96#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
97#[serde(rename_all = "camelCase")]
98#[non_exhaustive]
99pub struct EmissionItemInputV0 {
100 pub module_instance: ModuleInstanceKeyV0,
101 pub items: Vec<EmissionItemV0>,
102 pub disclosure: Vec<EmissionItemProjectionDisclosureV0>,
103}
104
105#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
106#[serde(rename_all = "camelCase")]
107#[non_exhaustive]
108pub struct EmissionItemOrderKeyV0 {
109 pub module_instance: ModuleInstanceKeyV0,
110 pub intra_module_ordinal: u32,
111}
112
113#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
114#[serde(rename_all = "camelCase")]
115#[non_exhaustive]
116pub struct EmissionItemPlanV0 {
117 pub policy: crate::EmissionOrderingPolicyV0,
118 pub entries: Vec<EmissionItemOrderKeyV0>,
119}
120
121#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
122#[serde(rename_all = "camelCase")]
123#[non_exhaustive]
124pub struct LinkedEmissionItemV0 {
125 pub global_order_index: u32,
126 pub module_instance: ModuleInstanceKeyV0,
127 pub kind: EmissionItemKindV0,
128 pub name: String,
129 pub range_start: u32,
130 pub range_end: u32,
131}
132
133#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
134#[serde(rename_all = "camelCase")]
135#[non_exhaustive]
136pub struct LinkedEmissionItemOrderV0 {
137 pub items: Vec<LinkedEmissionItemV0>,
138}
139
140#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
141#[serde(rename_all = "camelCase")]
142#[non_exhaustive]
143pub enum LinkedEmissionItemMaterializationErrorV0 {
144 LegacyMaterialization {
145 error: LinkedEmissionMaterializationErrorV0,
146 },
147 InvalidItemOrderIndex {
148 expected: u32,
149 actual: u32,
150 },
151 UnknownEmissionItemModule {
152 module_instance: ModuleInstanceKeyV0,
153 },
154 MissingEmissionItem {
155 module_instance: ModuleInstanceKeyV0,
156 },
157}
158
159impl From<LinkedEmissionMaterializationErrorV0> for LinkedEmissionItemMaterializationErrorV0 {
160 fn from(error: LinkedEmissionMaterializationErrorV0) -> Self {
161 Self::LegacyMaterialization { error }
162 }
163}
164
165#[derive(Debug, Clone, PartialEq, Eq)]
166#[non_exhaustive]
167pub struct TransformBundleEmissionItemProjectionV0 {
168 inputs: Vec<EmissionItemInputV0>,
169}
170
171impl TransformBundleEmissionItemProjectionV0 {
172 pub fn inputs(&self) -> &[EmissionItemInputV0] {
173 self.inputs.as_slice()
174 }
175
176 pub(crate) fn new(inputs: Vec<EmissionItemInputV0>) -> Self {
177 Self { inputs }
178 }
179}
180
181pub(crate) fn collect_emission_items(
182 facts: &ParsedStyleFacts,
183 emission_selectors: &ParsedEmissionSelectorFactsV0,
184) -> Vec<EmissionItemV0> {
185 let mut items = facts
186 .selectors
187 .iter()
188 .map(|selector| EmissionItemV0 {
189 kind: match selector.kind {
190 ParsedSelectorFactKind::Class => EmissionItemKindV0::SelectorClass,
191 ParsedSelectorFactKind::Id => EmissionItemKindV0::SelectorId,
192 ParsedSelectorFactKind::Placeholder => EmissionItemKindV0::SelectorPlaceholder,
193 },
194 name: selector.name.clone(),
195 range_start: u32::from(selector.range.start()),
196 range_end: u32::from(selector.range.end()),
197 })
198 .collect::<Vec<_>>();
199 for selector in &emission_selectors.selectors {
200 let kind = match selector.kind {
201 ParsedEmissionSelectorFactKindV0::Element => EmissionItemKindV0::SelectorElement,
202 ParsedEmissionSelectorFactKindV0::Attribute => EmissionItemKindV0::SelectorAttribute,
203 ParsedEmissionSelectorFactKindV0::Universal => EmissionItemKindV0::SelectorUniversal,
204 ParsedEmissionSelectorFactKindV0::PseudoClass => {
205 EmissionItemKindV0::SelectorPseudoClass
206 }
207 ParsedEmissionSelectorFactKindV0::PseudoElement => {
208 EmissionItemKindV0::SelectorPseudoElement
209 }
210 _ => EmissionItemKindV0::UnknownStructuralSelector,
211 };
212 items.push(EmissionItemV0 {
213 kind,
214 name: selector.name.clone(),
215 range_start: u32::from(selector.range.start()),
216 range_end: u32::from(selector.range.end()),
217 });
218 }
219 items.extend(facts.at_rules.iter().map(|at_rule| {
220 EmissionItemV0 {
221 kind: at_rule
222 .node_kind
223 .map_or(EmissionItemKindV0::UnknownAtRule, |node_kind| {
224 EmissionItemKindV0::AtRule {
225 node_kind: node_kind.as_u32(),
226 }
227 }),
228 name: at_rule.name.clone(),
229 range_start: u32::from(at_rule.range.start()),
230 range_end: u32::from(at_rule.range.end()),
231 }
232 }));
233 items.extend(facts.animations.iter().map(|animation| EmissionItemV0 {
234 kind: match animation.kind {
235 ParsedAnimationFactKind::KeyframesDeclaration => {
236 EmissionItemKindV0::KeyframesDeclaration
237 }
238 ParsedAnimationFactKind::AnimationNameReference => {
239 EmissionItemKindV0::AnimationNameReference
240 }
241 },
242 name: animation.name.clone(),
243 range_start: u32::from(animation.range.start()),
244 range_end: u32::from(animation.range.end()),
245 }));
246 items.sort_by_key(|item| {
247 (
248 item.range_start,
249 item.range_end,
250 item.kind,
251 item.name.clone(),
252 )
253 });
254 if items.is_empty() {
255 items.push(EmissionItemV0 {
256 kind: EmissionItemKindV0::ModuleBoundary,
257 name: String::new(),
258 range_start: 0,
259 range_end: 0,
260 });
261 }
262 items
263}
264
265pub(crate) fn emission_item_projection_disclosure(
266 facts: &ParsedStyleFacts,
267) -> Vec<EmissionItemProjectionDisclosureV0> {
268 let ParsedStyleFacts {
269 product: _,
270 dialect: _,
271 selector_count: _,
272 selectors: _,
273 variable_count: _,
274 variables: _,
275 sass_symbol_count: _,
276 sass_symbols: _,
277 sass_include_count: _,
278 sass_includes: _,
279 sass_module_edge_count: _,
280 sass_module_edges: _,
281 sass_placeholder_definition_count: _,
282 sass_placeholder_definitions: _,
283 extend_target_count: _,
284 extend_targets: _,
285 animation_count: _,
286 animations: _,
287 css_module_value_count: _,
288 css_module_values: _,
289 css_module_value_import_edge_count: _,
290 css_module_value_import_edges: _,
291 css_module_value_definition_edge_count: _,
292 css_module_value_definition_edges: _,
293 css_module_composes_count: _,
294 css_module_composes: _,
295 css_module_composes_edge_count: _,
296 css_module_composes_edges: _,
297 icss_count: _,
298 icss: _,
299 icss_import_edge_count: _,
300 icss_import_edges: _,
301 icss_export_edge_count: _,
302 icss_export_edges: _,
303 at_rule_count: _,
304 at_rules: _,
305 error_count: _,
306 } = facts;
307
308 use EmissionItemFactCategoryV0 as Category;
309 use EmissionItemProjectionDispositionV0 as Disposition;
310 use EmissionItemProjectionReasonV0 as Reason;
311
312 [
313 (
314 Category::NamedSelectors,
315 Disposition::Projected,
316 Reason::SourceOrderedNamedSelector,
317 ),
318 (
319 Category::EmissionSelectors,
320 Disposition::Projected,
321 Reason::SourceOrderedStructuralSelector,
322 ),
323 (
324 Category::AtRules,
325 Disposition::Projected,
326 Reason::SourceOrderedAtRule,
327 ),
328 (
329 Category::Animations,
330 Disposition::Projected,
331 Reason::SourceOrderedAnimation,
332 ),
333 (
334 Category::Variables,
335 Disposition::NotProjected,
336 Reason::NotAnEmissionBoundary,
337 ),
338 (
339 Category::SassSymbols,
340 Disposition::NotProjected,
341 Reason::NotAnEmissionBoundary,
342 ),
343 (
344 Category::SassIncludes,
345 Disposition::NotProjected,
346 Reason::NotAnEmissionBoundary,
347 ),
348 (
349 Category::SassModuleEdges,
350 Disposition::NotProjected,
351 Reason::NotAnEmissionBoundary,
352 ),
353 (
354 Category::SassPlaceholderDefinitions,
355 Disposition::NotProjected,
356 Reason::NotAnEmissionBoundary,
357 ),
358 (
359 Category::ExtendTargets,
360 Disposition::NotProjected,
361 Reason::NotAnEmissionBoundary,
362 ),
363 (
364 Category::CssModuleValues,
365 Disposition::NotProjected,
366 Reason::NotAnEmissionBoundary,
367 ),
368 (
369 Category::CssModuleValueImportEdges,
370 Disposition::NotProjected,
371 Reason::NotAnEmissionBoundary,
372 ),
373 (
374 Category::CssModuleValueDefinitionEdges,
375 Disposition::NotProjected,
376 Reason::NotAnEmissionBoundary,
377 ),
378 (
379 Category::CssModuleComposes,
380 Disposition::NotProjected,
381 Reason::NotAnEmissionBoundary,
382 ),
383 (
384 Category::CssModuleComposesEdges,
385 Disposition::NotProjected,
386 Reason::NotAnEmissionBoundary,
387 ),
388 (
389 Category::Icss,
390 Disposition::NotProjected,
391 Reason::NotAnEmissionBoundary,
392 ),
393 (
394 Category::IcssImportEdges,
395 Disposition::NotProjected,
396 Reason::NotAnEmissionBoundary,
397 ),
398 (
399 Category::IcssExportEdges,
400 Disposition::NotProjected,
401 Reason::NotAnEmissionBoundary,
402 ),
403 ]
404 .into_iter()
405 .map(
406 |(category, disposition, reason)| EmissionItemProjectionDisclosureV0 {
407 category,
408 disposition,
409 reason,
410 },
411 )
412 .collect()
413}
414
415pub(crate) fn build_emission_item_plan(
416 inputs: &[EmissionItemInputV0],
417 module_plan: &EmissionModulePlanV0,
418) -> Result<EmissionItemPlanV0, TransformBundleLinkErrorV0> {
419 let inputs_by_instance = inputs
420 .iter()
421 .map(|input| (input.module_instance.clone(), input))
422 .collect::<BTreeMap<_, _>>();
423 if inputs_by_instance.len() != inputs.len() {
424 return Err(TransformBundleLinkErrorV0::InvalidEmissionPlan {
425 reason: "emission-item inputs contain a duplicate module instance".to_string(),
426 });
427 }
428 let mut entries = Vec::new();
429 for instance in &module_plan.module_order {
430 let input = inputs_by_instance.get(instance).ok_or_else(|| {
431 TransformBundleLinkErrorV0::InvalidEmissionPlan {
432 reason: format!(
433 "reachable module {} has no emission-item input",
434 instance.module().as_str()
435 ),
436 }
437 })?;
438 for intra_module_ordinal in 0..input.items.len() {
439 entries.push(EmissionItemOrderKeyV0 {
440 module_instance: instance.clone(),
441 intra_module_ordinal: u32::try_from(intra_module_ordinal).map_err(|_| {
442 TransformBundleLinkErrorV0::InvalidEmissionPlan {
443 reason: format!(
444 "module {} has more emission items than the order key can represent",
445 instance.module().as_str()
446 ),
447 }
448 })?,
449 });
450 }
451 }
452 Ok(EmissionItemPlanV0 {
453 policy: module_plan.policy,
454 entries,
455 })
456}
457
458pub(crate) fn build_linked_emission_item_order(
459 inputs: &[EmissionItemInputV0],
460 plan: &EmissionItemPlanV0,
461) -> Result<LinkedEmissionItemOrderV0, TransformBundleLinkErrorV0> {
462 let inputs_by_instance = inputs
463 .iter()
464 .map(|input| (input.module_instance.clone(), input))
465 .collect::<BTreeMap<_, _>>();
466 let mut items = Vec::with_capacity(plan.entries.len());
467 for (global_order_index, key) in plan.entries.iter().enumerate() {
468 let input = inputs_by_instance
469 .get(&key.module_instance)
470 .ok_or_else(|| TransformBundleLinkErrorV0::InvalidEmissionPlan {
471 reason: format!(
472 "emission-item key refers to unknown module {}",
473 key.module_instance.module().as_str()
474 ),
475 })?;
476 let item = input
477 .items
478 .get(key.intra_module_ordinal as usize)
479 .ok_or_else(|| TransformBundleLinkErrorV0::InvalidEmissionPlan {
480 reason: format!(
481 "emission-item key refers to missing item {} in {}",
482 key.intra_module_ordinal,
483 key.module_instance.module().as_str()
484 ),
485 })?;
486 items.push(LinkedEmissionItemV0 {
487 global_order_index: u32::try_from(global_order_index).map_err(|_| {
488 TransformBundleLinkErrorV0::InvalidEmissionPlan {
489 reason: "emission-item plan exceeds the output index range".to_string(),
490 }
491 })?,
492 module_instance: key.module_instance.clone(),
493 kind: item.kind,
494 name: item.name.clone(),
495 range_start: item.range_start,
496 range_end: item.range_end,
497 });
498 }
499 Ok(LinkedEmissionItemOrderV0 { items })
500}
501
502pub(crate) fn build_global_rule_order_from_emission_items(
503 order: &LinkedEmissionItemOrderV0,
504) -> Result<GlobalRuleOrderV0, TransformBundleLinkErrorV0> {
505 let mut rules = Vec::new();
506 for item in &order.items {
507 let selector_kind = match item.kind {
508 EmissionItemKindV0::SelectorClass => "class",
509 EmissionItemKindV0::SelectorId => "id",
510 EmissionItemKindV0::SelectorPlaceholder => "placeholder",
511 _ => continue,
512 };
513 rules.push(LinkedStylesheetRuleV0 {
514 global_order_index: u32::try_from(rules.len()).map_err(|_| {
515 TransformBundleLinkErrorV0::InvalidEmissionPlan {
516 reason: "selector projection exceeds the output index range".to_string(),
517 }
518 })?,
519 module_instance: item.module_instance.clone(),
520 selector_name: item.name.clone(),
521 selector_kind,
522 range_start: item.range_start,
523 range_end: item.range_end,
524 });
525 }
526 Ok(GlobalRuleOrderV0 { rules })
527}
528
529#[cfg(test)]
530mod tests {
531 use std::collections::BTreeSet;
532
533 use omena_parser::{StyleDialect, collect_style_fact_collection};
534
535 use super::{
536 EmissionItemFactCategoryV0, EmissionItemKindV0, collect_emission_items,
537 emission_item_projection_disclosure,
538 };
539
540 fn items_for(source: &str) -> Vec<super::EmissionItemV0> {
541 let collection = collect_style_fact_collection(source, StyleDialect::Css);
542 collect_emission_items(&collection.facts, &collection.emission_selectors)
543 }
544
545 #[test]
546 fn selector_less_stylesheets_contribute_emission_items() {
547 for source in [
548 "main { color: red; }",
549 "@font-face { font-family: Demo; src: url(demo.woff2); }",
550 "@layer reset, theme;",
551 ] {
552 assert!(
553 !items_for(source).is_empty(),
554 "expected an emission item for {source}"
555 );
556 }
557 }
558
559 #[test]
560 fn emission_items_for_empty_stylesheets_are_module_boundaries() {
561 for source in ["", "/* license */"] {
562 assert_eq!(
563 items_for(source),
564 vec![super::EmissionItemV0 {
565 kind: EmissionItemKindV0::ModuleBoundary,
566 name: String::new(),
567 range_start: 0,
568 range_end: 0,
569 }]
570 );
571 }
572 }
573
574 #[test]
575 fn emission_items_follow_source_order_across_fact_categories() {
576 let items = items_for(
577 ":root { --brand: red; }\n\
578 @layer reset;\n\
579 div, .card, [hidden], *::before { color: red; }\n\
580 @keyframes pulse { from { opacity: 0; } }",
581 );
582
583 assert!(items.windows(2).all(|pair| {
584 (pair[0].range_start, pair[0].range_end) <= (pair[1].range_start, pair[1].range_end)
585 }));
586 for kind in [
587 EmissionItemKindV0::SelectorPseudoClass,
588 EmissionItemKindV0::SelectorElement,
589 EmissionItemKindV0::SelectorClass,
590 EmissionItemKindV0::SelectorAttribute,
591 EmissionItemKindV0::SelectorUniversal,
592 EmissionItemKindV0::SelectorPseudoElement,
593 EmissionItemKindV0::KeyframesDeclaration,
594 ] {
595 assert!(
596 items.iter().any(|item| item.kind == kind),
597 "missing emission item kind {kind:?}"
598 );
599 }
600 assert!(
601 items
602 .iter()
603 .any(|item| matches!(item.kind, EmissionItemKindV0::AtRule { .. }))
604 );
605 }
606
607 #[test]
608 fn emission_items_for_supported_syntax_have_no_unknown_kinds() {
609 for source in [
610 "main, [hidden], *, :hover, ::before { color: red; }",
611 "@font-face { font-family: Demo; src: url(demo.woff2); }",
612 "@layer reset, theme; @media (width > 1px) { div { color: red; } }",
613 "@supports (display: grid) { .grid { display: grid; } }",
614 "@keyframes pulse { from { opacity: 0; } to { opacity: 1; } }",
615 ] {
616 let unknown = items_for(source)
617 .into_iter()
618 .filter(|item| {
619 matches!(
620 item.kind,
621 EmissionItemKindV0::UnknownStructuralSelector
622 | EmissionItemKindV0::UnknownAtRule
623 )
624 })
625 .collect::<Vec<_>>();
626 assert!(
627 unknown.is_empty(),
628 "unknown emission items for {source}: {unknown:?}"
629 );
630 }
631 }
632
633 #[test]
634 fn projection_disclosure_is_total_over_fact_categories() {
635 let collection = collect_style_fact_collection(".card { color: red; }", StyleDialect::Css);
636 let disclosure = emission_item_projection_disclosure(&collection.facts);
637 let categories = disclosure
638 .iter()
639 .map(|entry| entry.category)
640 .collect::<BTreeSet<_>>();
641
642 assert_eq!(disclosure.len(), 18);
643 assert_eq!(categories.len(), disclosure.len());
644 assert!(categories.contains(&EmissionItemFactCategoryV0::NamedSelectors));
645 assert!(categories.contains(&EmissionItemFactCategoryV0::EmissionSelectors));
646 assert!(categories.contains(&EmissionItemFactCategoryV0::AtRules));
647 assert!(categories.contains(&EmissionItemFactCategoryV0::Animations));
648 }
649}