@@ -41,6 +41,7 @@ use oxc_parser::Parser;
4141use oxc_span:: { GetSpan , SourceType } ;
4242
4343use crate :: optimizer:: Edit ;
44+ use crate :: pipeline:: selector:: { R3SelectorElement , parse_selector_to_r3_selector} ;
4445
4546/// Partial declaration function names to link.
4647const DECLARE_FACTORY : & str = "\u{0275} \u{0275} ngDeclareFactory" ;
@@ -532,67 +533,24 @@ fn extract_deps_source(obj: &ObjectExpression<'_>, source: &str, ns: &str) -> St
532533/// - `"[attr=value]"` → `[["", "attr", "value"]]`
533534/// - `"div[ngClass]"` → `[["div", "ngClass", ""]]`
534535/// - `"[a],[b]"` → `[["", "a", ""], ["", "b", ""]]`
535- /// - `".cls"` → `[["", "class", "cls"]]`
536+ /// - `".cls"` → `[["", 8, "cls"]]`
537+ /// - `"ng-scrollbar:not([externalViewport])"` → `[["ng-scrollbar", 3, "externalViewport", ""]]`
536538fn parse_selector ( selector : & str ) -> String {
537- let selectors: Vec < String > =
538- selector. split ( ',' ) . map ( |s| parse_single_selector ( s. trim ( ) ) ) . collect ( ) ;
539- format ! ( "[{}]" , selectors. join( ", " ) )
540- }
541-
542- /// Parse a single selector (no commas) into Angular's array format.
543- fn parse_single_selector ( selector : & str ) -> String {
544- let mut parts: Vec < String > = Vec :: new ( ) ;
545- let mut remaining = selector;
546-
547- // Extract tag name (everything before first [ or . or :)
548- let tag_end = remaining
549- . find ( |c : char | c == '[' || c == '.' || c == ':' || c == '#' )
550- . unwrap_or ( remaining. len ( ) ) ;
551- let tag = & remaining[ ..tag_end] ;
552- remaining = & remaining[ tag_end..] ;
553-
554- if !tag. is_empty ( ) {
555- parts. push ( format ! ( "\" {}\" " , tag) ) ;
556- } else {
557- parts. push ( "\" \" " . to_string ( ) ) ;
558- }
559-
560- // Extract attribute selectors [attr] or [attr=value]
561- while let Some ( bracket_start) = remaining. find ( '[' ) {
562- let bracket_end = remaining[ bracket_start..] . find ( ']' ) . map ( |i| bracket_start + i) ;
563- if let Some ( end) = bracket_end {
564- let attr_content = & remaining[ bracket_start + 1 ..end] ;
565- if let Some ( eq_pos) = attr_content. find ( '=' ) {
566- let attr_name = & attr_content[ ..eq_pos] ;
567- let attr_value = attr_content[ eq_pos + 1 ..] . trim_matches ( '"' ) . trim_matches ( '\'' ) ;
568- parts. push ( format ! ( "\" {}\" " , attr_name) ) ;
569- parts. push ( format ! ( "\" {}\" " , attr_value) ) ;
570- } else {
571- parts. push ( format ! ( "\" {}\" " , attr_content) ) ;
572- parts. push ( "\" \" " . to_string ( ) ) ;
573- }
574- remaining = & remaining[ end + 1 ..] ;
575- } else {
576- break ;
577- }
578- }
579-
580- // Extract class selectors .className
581- let mut class_remaining = remaining;
582- while let Some ( dot_pos) = class_remaining. find ( '.' ) {
583- let class_end = class_remaining[ dot_pos + 1 ..]
584- . find ( |c : char | !c. is_alphanumeric ( ) && c != '-' && c != '_' )
585- . map ( |i| dot_pos + 1 + i)
586- . unwrap_or ( class_remaining. len ( ) ) ;
587- let class_name = & class_remaining[ dot_pos + 1 ..class_end] ;
588- if !class_name. is_empty ( ) {
589- parts. push ( "\" class\" " . to_string ( ) ) ;
590- parts. push ( format ! ( "\" {}\" " , class_name) ) ;
591- }
592- class_remaining = & class_remaining[ class_end..] ;
593- }
594-
595- format ! ( "[{}]" , parts. join( ", " ) )
539+ let r3_selectors = parse_selector_to_r3_selector ( selector) ;
540+ let selector_strs: Vec < String > = r3_selectors
541+ . iter ( )
542+ . map ( |elements| {
543+ let parts: Vec < String > = elements
544+ . iter ( )
545+ . map ( |el| match el {
546+ R3SelectorElement :: String ( s) => format ! ( "\" {}\" " , s) ,
547+ R3SelectorElement :: Flag ( f) => f. to_string ( ) ,
548+ } )
549+ . collect ( ) ;
550+ format ! ( "[{}]" , parts. join( ", " ) )
551+ } )
552+ . collect ( ) ;
553+ format ! ( "[{}]" , selector_strs. join( ", " ) )
596554}
597555
598556/// Build the `hostAttrs` flat array from the partial declaration's `host` object.
@@ -1635,14 +1593,83 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImpor
16351593
16361594 #[ test]
16371595 fn test_parse_selector_class ( ) {
1638- assert_eq ! ( parse_selector( ".my-class" ) , r#"[["", "class", "my-class"]]"# ) ;
1596+ // Classes use SelectorFlags.CLASS (8) instead of "class" string
1597+ assert_eq ! ( parse_selector( ".my-class" ) , r#"[["", 8, "my-class"]]"# ) ;
16391598 }
16401599
16411600 #[ test]
16421601 fn test_parse_selector_multiple ( ) {
16431602 assert_eq ! ( parse_selector( "[a],[b]" ) , r#"[["", "a", ""], ["", "b", ""]]"# ) ;
16441603 }
16451604
1605+ #[ test]
1606+ fn test_parse_selector_not_attribute ( ) {
1607+ // :not() with attribute - SelectorFlags.NOT | SelectorFlags.ATTRIBUTE = 3
1608+ assert_eq ! (
1609+ parse_selector( "ng-scrollbar:not([externalViewport])" ) ,
1610+ r#"[["ng-scrollbar", 3, "externalViewport", ""]]"#
1611+ ) ;
1612+ }
1613+
1614+ #[ test]
1615+ fn test_parse_selector_not_attribute_with_value ( ) {
1616+ assert_eq ! (
1617+ parse_selector( "input:not([type=checkbox])" ) ,
1618+ r#"[["input", 3, "type", "checkbox"]]"#
1619+ ) ;
1620+ }
1621+
1622+ #[ test]
1623+ fn test_parse_selector_multiple_not ( ) {
1624+ // Multiple :not() clauses
1625+ assert_eq ! (
1626+ parse_selector( "[ngModel]:not([formControlName]):not([formControl])" ) ,
1627+ r#"[["", "ngModel", "", 3, "formControlName", "", 3, "formControl", ""]]"#
1628+ ) ;
1629+ }
1630+
1631+ #[ test]
1632+ fn test_parse_selector_not_element ( ) {
1633+ // :not() with element - SelectorFlags.NOT | SelectorFlags.ELEMENT = 5
1634+ assert_eq ! ( parse_selector( ":not(span)" ) , r#"[["", 5, "span"]]"# ) ;
1635+ }
1636+
1637+ #[ test]
1638+ fn test_parse_selector_not_class ( ) {
1639+ // :not() with class - SelectorFlags.NOT | SelectorFlags.CLASS = 9
1640+ assert_eq ! ( parse_selector( ":not(.hidden)" ) , r#"[["", 9, "hidden"]]"# ) ;
1641+ }
1642+
1643+ #[ test]
1644+ fn test_parse_selector_complex_not ( ) {
1645+ // Complex: element + class + attribute + multiple :not()
1646+ assert_eq ! (
1647+ parse_selector( "div.foo[some-directive]:not([title]):not(.baz)" ) ,
1648+ r#"[["div", "some-directive", "", 8, "foo", 3, "title", "", 9, "baz"]]"#
1649+ ) ;
1650+ }
1651+
1652+ #[ test]
1653+ fn test_parse_selector_element_with_class_and_attribute ( ) {
1654+ // Class should come after attributes with CLASS flag
1655+ assert_eq ! ( parse_selector( "div.active[role]" ) , r#"[["div", "role", "", 8, "active"]]"# ) ;
1656+ }
1657+
1658+ #[ test]
1659+ fn test_parse_selector_not_only ( ) {
1660+ // Only :not() selectors - element becomes "*" but emitted as ""
1661+ assert_eq ! ( parse_selector( ":not(.hidden)" ) , r#"[["", 9, "hidden"]]"# ) ;
1662+ }
1663+
1664+ #[ test]
1665+ fn test_parse_selector_comma_with_not ( ) {
1666+ // Comma-separated selectors with :not()
1667+ assert_eq ! (
1668+ parse_selector( "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]" ) ,
1669+ r#"[["form", 3, "ngNoForm", "", 3, "formGroup", ""], ["ng-form"], ["", "ngForm", ""]]"#
1670+ ) ;
1671+ }
1672+
16461673 #[ test]
16471674 fn test_no_declarations ( ) {
16481675 let allocator = Allocator :: default ( ) ;
0 commit comments