@@ -1395,13 +1395,31 @@ pub trait R3Visitor<'a> {
13951395 /// Visit a bound text node.
13961396 fn visit_bound_text ( & mut self , _text : & R3BoundText < ' a > ) { }
13971397
1398+ /// Visit a static text attribute.
1399+ fn visit_text_attribute ( & mut self , _attr : & R3TextAttribute < ' a > ) { }
1400+
1401+ /// Visit a bound attribute (input property).
1402+ fn visit_bound_attribute ( & mut self , _attr : & R3BoundAttribute < ' a > ) { }
1403+
1404+ /// Visit a bound event (output).
1405+ fn visit_bound_event ( & mut self , _event : & R3BoundEvent < ' a > ) { }
1406+
13981407 /// Visit an element.
13991408 fn visit_element ( & mut self , element : & R3Element < ' a > ) {
14001409 self . visit_element_children ( element) ;
14011410 }
14021411
1403- /// Visit element children.
1412+ /// Visit element children, attributes, inputs, and outputs .
14041413 fn visit_element_children ( & mut self , element : & R3Element < ' a > ) {
1414+ for attr in & element. attributes {
1415+ self . visit_text_attribute ( attr) ;
1416+ }
1417+ for input in & element. inputs {
1418+ self . visit_bound_attribute ( input) ;
1419+ }
1420+ for output in & element. outputs {
1421+ self . visit_bound_event ( output) ;
1422+ }
14051423 for child in & element. children {
14061424 child. visit ( self ) ;
14071425 }
@@ -1412,15 +1430,27 @@ pub trait R3Visitor<'a> {
14121430 self . visit_template_children ( template) ;
14131431 }
14141432
1415- /// Visit template children.
1433+ /// Visit template children, attributes, inputs, and outputs .
14161434 fn visit_template_children ( & mut self , template : & R3Template < ' a > ) {
1435+ for attr in & template. attributes {
1436+ self . visit_text_attribute ( attr) ;
1437+ }
1438+ for input in & template. inputs {
1439+ self . visit_bound_attribute ( input) ;
1440+ }
1441+ for output in & template. outputs {
1442+ self . visit_bound_event ( output) ;
1443+ }
14171444 for child in & template. children {
14181445 child. visit ( self ) ;
14191446 }
14201447 }
14211448
14221449 /// Visit a content projection slot.
14231450 fn visit_content ( & mut self , content : & R3Content < ' a > ) {
1451+ for attr in & content. attributes {
1452+ self . visit_text_attribute ( attr) ;
1453+ }
14241454 for child in & content. children {
14251455 child. visit ( self ) ;
14261456 }
@@ -1531,6 +1561,15 @@ pub trait R3Visitor<'a> {
15311561
15321562 /// Visit a component.
15331563 fn visit_component ( & mut self , component : & R3Component < ' a > ) {
1564+ for attr in & component. attributes {
1565+ self . visit_text_attribute ( attr) ;
1566+ }
1567+ for input in & component. inputs {
1568+ self . visit_bound_attribute ( input) ;
1569+ }
1570+ for output in & component. outputs {
1571+ self . visit_bound_event ( output) ;
1572+ }
15341573 for child in & component. children {
15351574 child. visit ( self ) ;
15361575 }
@@ -1568,3 +1607,125 @@ pub struct R3ParseResult<'a> {
15681607 /// Comment nodes (if collected).
15691608 pub comment_nodes : Option < Vec < ' a , R3Comment < ' a > > > ,
15701609}
1610+
1611+ #[ cfg( test) ]
1612+ mod tests {
1613+ use oxc_allocator:: Allocator ;
1614+
1615+ use crate :: ast:: r3:: { R3Visitor , visit_all} ;
1616+ use crate :: parser:: html:: HtmlParser ;
1617+ use crate :: transform:: html_to_r3:: { TransformOptions , html_ast_to_r3_ast} ;
1618+
1619+ /// A visitor that collects names of visited attributes, inputs, and outputs.
1620+ struct AttributeCollector {
1621+ text_attributes : Vec < String > ,
1622+ bound_attributes : Vec < String > ,
1623+ bound_events : Vec < String > ,
1624+ elements : Vec < String > ,
1625+ }
1626+
1627+ impl AttributeCollector {
1628+ fn new ( ) -> Self {
1629+ Self {
1630+ text_attributes : Vec :: new ( ) ,
1631+ bound_attributes : Vec :: new ( ) ,
1632+ bound_events : Vec :: new ( ) ,
1633+ elements : Vec :: new ( ) ,
1634+ }
1635+ }
1636+ }
1637+
1638+ impl < ' a > R3Visitor < ' a > for AttributeCollector {
1639+ fn visit_element ( & mut self , element : & super :: R3Element < ' a > ) {
1640+ self . elements . push ( element. name . to_string ( ) ) ;
1641+ self . visit_element_children ( element) ;
1642+ }
1643+
1644+ fn visit_text_attribute ( & mut self , attr : & super :: R3TextAttribute < ' a > ) {
1645+ self . text_attributes . push ( attr. name . to_string ( ) ) ;
1646+ }
1647+
1648+ fn visit_bound_attribute ( & mut self , attr : & super :: R3BoundAttribute < ' a > ) {
1649+ self . bound_attributes . push ( attr. name . to_string ( ) ) ;
1650+ }
1651+
1652+ fn visit_bound_event ( & mut self , event : & super :: R3BoundEvent < ' a > ) {
1653+ self . bound_events . push ( event. name . to_string ( ) ) ;
1654+ }
1655+ }
1656+
1657+ #[ test]
1658+ fn test_r3_visitor_visits_attributes_inputs_outputs ( ) {
1659+ let allocator = Allocator :: default ( ) ;
1660+ let template =
1661+ r#"<button type="submit" [disabled]="isDisabled" (click)="onClick()">Save</button>"# ;
1662+
1663+ let html_result = HtmlParser :: new ( & allocator, template, "test.html" ) . parse ( ) ;
1664+ assert ! ( html_result. errors. is_empty( ) ) ;
1665+
1666+ let r3_result = html_ast_to_r3_ast (
1667+ & allocator,
1668+ template,
1669+ & html_result. nodes ,
1670+ TransformOptions :: default ( ) ,
1671+ ) ;
1672+ assert ! ( r3_result. errors. is_empty( ) ) ;
1673+
1674+ let mut collector = AttributeCollector :: new ( ) ;
1675+ visit_all ( & mut collector, & r3_result. nodes ) ;
1676+
1677+ assert_eq ! ( collector. elements, vec![ "button" ] ) ;
1678+ assert_eq ! ( collector. text_attributes, vec![ "type" ] ) ;
1679+ assert_eq ! ( collector. bound_attributes, vec![ "disabled" ] ) ;
1680+ assert_eq ! ( collector. bound_events, vec![ "click" ] ) ;
1681+ }
1682+
1683+ #[ test]
1684+ fn test_r3_visitor_visits_nested_elements ( ) {
1685+ let allocator = Allocator :: default ( ) ;
1686+ let template = r#"<div id="outer"><span class="inner" [title]="t" (mouseenter)="onHover()">text</span></div>"# ;
1687+
1688+ let html_result = HtmlParser :: new ( & allocator, template, "test.html" ) . parse ( ) ;
1689+ assert ! ( html_result. errors. is_empty( ) ) ;
1690+
1691+ let r3_result = html_ast_to_r3_ast (
1692+ & allocator,
1693+ template,
1694+ & html_result. nodes ,
1695+ TransformOptions :: default ( ) ,
1696+ ) ;
1697+ assert ! ( r3_result. errors. is_empty( ) ) ;
1698+
1699+ let mut collector = AttributeCollector :: new ( ) ;
1700+ visit_all ( & mut collector, & r3_result. nodes ) ;
1701+
1702+ assert_eq ! ( collector. elements, vec![ "div" , "span" ] ) ;
1703+ assert_eq ! ( collector. text_attributes, vec![ "id" , "class" ] ) ;
1704+ assert_eq ! ( collector. bound_attributes, vec![ "title" ] ) ;
1705+ assert_eq ! ( collector. bound_events, vec![ "mouseenter" ] ) ;
1706+ }
1707+
1708+ #[ test]
1709+ fn test_r3_visitor_default_noop_does_not_break ( ) {
1710+ let allocator = Allocator :: default ( ) ;
1711+ let template = r#"<input [value]="name" (change)="update()" required />"# ;
1712+
1713+ let html_result = HtmlParser :: new ( & allocator, template, "test.html" ) . parse ( ) ;
1714+ assert ! ( html_result. errors. is_empty( ) ) ;
1715+
1716+ let r3_result = html_ast_to_r3_ast (
1717+ & allocator,
1718+ template,
1719+ & html_result. nodes ,
1720+ TransformOptions :: default ( ) ,
1721+ ) ;
1722+ assert ! ( r3_result. errors. is_empty( ) ) ;
1723+
1724+ // A visitor with all default no-op methods should traverse without panic
1725+ struct NoopVisitor ;
1726+ impl < ' a > R3Visitor < ' a > for NoopVisitor { }
1727+
1728+ let mut visitor = NoopVisitor ;
1729+ visit_all ( & mut visitor, & r3_result. nodes ) ;
1730+ }
1731+ }
0 commit comments