@@ -495,23 +495,28 @@ function matchesNodeDeep(root: AriaNode, template: AriaTemplateNode, collectAll:
495495 return results ;
496496}
497497
498+ function buildByRefMap ( root : AriaNode | undefined , map : Map < string , AriaNode > = new Map ( ) ) : Map < string , AriaNode > {
499+ if ( root ?. ref )
500+ map . set ( root . ref , root ) ;
501+ for ( const child of root ?. children || [ ] ) {
502+ if ( typeof child !== 'string' )
503+ buildByRefMap ( child , map ) ;
504+ }
505+ return map ;
506+ }
507+
508+ function arePropsEqual ( a : AriaNode , b : AriaNode ) : boolean {
509+ const aKeys = Object . keys ( a . props ) ;
510+ const bKeys = Object . keys ( b . props ) ;
511+ return aKeys . length === bKeys . length && aKeys . every ( k => a . props [ k ] === b . props [ k ] ) ;
512+ }
513+
498514export function renderAriaTree ( ariaSnapshot : AriaSnapshot , publicOptions : AriaTreeOptions , previous ?: AriaSnapshot ) : string {
499515 const options = toInternalOptions ( publicOptions ) ;
500516 const lines : string [ ] = [ ] ;
501517 const includeText = options . renderStringsAsRegex ? textContributesInfo : ( ) => true ;
502518 const renderString = options . renderStringsAsRegex ? convertToBestGuessRegex : ( str : string ) => str ;
503-
504- const previousByRef = new Map < string , AriaNode > ( ) ;
505- const visitPrevious = ( ariaNode : AriaNode ) => {
506- if ( ariaNode . ref )
507- previousByRef . set ( ariaNode . ref , ariaNode ) ;
508- for ( const child of ariaNode . children ) {
509- if ( typeof child !== 'string' )
510- visitPrevious ( child ) ;
511- }
512- } ;
513- if ( previous )
514- visitPrevious ( previous . root ) ;
519+ const previousByRef = buildByRefMap ( previous ?. root ) ;
515520
516521 const visitText = ( text : string , indent : string ) => {
517522 const escaped = yamlEscapeValueIfNeeded ( renderString ( text ) ) ;
@@ -556,40 +561,36 @@ export function renderAriaTree(ariaSnapshot: AriaSnapshot, publicOptions: AriaTr
556561 return key ;
557562 } ;
558563
559- const arePropsEqual = ( a : AriaNode , b : AriaNode ) : boolean => {
560- const aKeys = Object . keys ( a . props ) ;
561- const bKeys = Object . keys ( b . props ) ;
562- return aKeys . length === bKeys . length && aKeys . every ( k => a . props [ k ] === b . props [ k ] ) ;
564+ const getSingleInlinedTextChild = ( ariaNode : AriaNode | undefined ) : string | undefined => {
565+ return ariaNode ?. children . length === 1 && typeof ariaNode . children [ 0 ] === 'string' && ! Object . keys ( ariaNode . props ) . length ? ariaNode . children [ 0 ] : undefined ;
563566 } ;
564567
565568 const visit = ( ariaNode : AriaNode , indent : string , renderCursorPointer : boolean , previousNode : AriaNode | undefined ) : { unchanged : boolean } => {
566569 if ( ariaNode . ref )
567570 previousNode = previousByRef . get ( ariaNode . ref ) ;
568- const linesBefore = lines . length ;
569571
572+ const linesBefore = lines . length ;
570573 const key = createKey ( ariaNode , renderCursorPointer ) ;
571- let unchanged = ! ! previousNode && key === createKey ( previousNode , renderCursorPointer ) && arePropsEqual ( ariaNode , previousNode ) ;
572-
573574 const escapedKey = indent + '- ' + yamlEscapeKeyIfNeeded ( key ) ;
574- const hasProps = ! ! Object . keys ( ariaNode . props ) . length ;
575575 const inCursorPointer = renderCursorPointer && ! ! ariaNode . ref && hasPointerCursor ( ariaNode ) ;
576+ const singleInlinedTextChild = getSingleInlinedTextChild ( ariaNode ) ;
577+
578+ // Whether ariaNode's subtree is the same as previousNode's, and can be replaced with just a ref.
579+ let unchanged = ! ! previousNode && key === createKey ( previousNode , renderCursorPointer ) && arePropsEqual ( ariaNode , previousNode ) ;
576580
577- if ( ! ariaNode . children . length && ! hasProps ) {
581+ if ( ! ariaNode . children . length && ! Object . keys ( ariaNode . props ) . length ) {
578582 // Leaf node without children.
579583 lines . push ( escapedKey ) ;
580- } else if ( ariaNode . children . length === 1 && typeof ariaNode . children [ 0 ] === 'string' && ! hasProps ) {
581- // Leaf node with only some text inside.
582- const shouldInclude = includeText ( ariaNode , ariaNode . children [ 0 ] ) ;
583- const text = shouldInclude ? renderString ( ariaNode . children [ 0 ] ) : null ;
584- if ( text )
585- lines . push ( escapedKey + ': ' + yamlEscapeValueIfNeeded ( text ) ) ;
584+ } else if ( singleInlinedTextChild !== undefined ) {
585+ // Leaf node with just some text inside.
586+ // Unchanged when the previous node also had the same single text child.
587+ unchanged = unchanged && getSingleInlinedTextChild ( previousNode ) === singleInlinedTextChild ;
588+
589+ const shouldInclude = includeText ( ariaNode , singleInlinedTextChild ) ;
590+ if ( shouldInclude )
591+ lines . push ( escapedKey + ': ' + yamlEscapeValueIfNeeded ( renderString ( singleInlinedTextChild ) ) ) ;
586592 else
587593 lines . push ( escapedKey ) ;
588-
589- // Node is unchanged only when previous node also had the same single text child.
590- unchanged = unchanged && ! ! previousNode &&
591- previousNode . children . length === 1 && typeof previousNode . children [ 0 ] === 'string' &&
592- ! Object . keys ( previousNode . props ) . length && ariaNode . children [ 0 ] === previousNode . children [ 0 ] ;
593594 } else {
594595 // Node with (optional) props and some children.
595596 lines . push ( escapedKey + ':' ) ;
@@ -603,10 +604,9 @@ export function renderAriaTree(ariaSnapshot: AriaSnapshot, publicOptions: AriaTr
603604 for ( let childIndex = 0 ; childIndex < ariaNode . children . length ; childIndex ++ ) {
604605 const child = ariaNode . children [ childIndex ] ;
605606 if ( typeof child === 'string' ) {
606- const shouldInclude = includeText ( ariaNode , child ) ;
607- if ( shouldInclude )
607+ unchanged = unchanged && previousNode ?. children [ childIndex ] === child ;
608+ if ( includeText ( ariaNode , child ) )
608609 visitText ( child , childIndent ) ;
609- unchanged = unchanged && previousNode ?. children [ childIndex ] === child && shouldInclude === includeText ( previousNode , child ) ;
610610 } else {
611611 const previousChild = previousNode ?. children [ childIndex ] ;
612612 const childResult = visit ( child , childIndent , renderCursorPointer && ! inCursorPointer , typeof previousChild !== 'string' ? previousChild : undefined ) ;
@@ -624,17 +624,13 @@ export function renderAriaTree(ariaSnapshot: AriaSnapshot, publicOptions: AriaTr
624624 return { unchanged } ;
625625 } ;
626626
627- const ariaNode = ariaSnapshot . root ;
628- if ( ariaNode . role === 'fragment' ) {
629- // Render fragment.
630- for ( const child of ariaNode . children || [ ] ) {
631- if ( typeof child === 'string' )
632- visitText ( child , '' ) ;
633- else
634- visit ( child , '' , ! ! options . renderCursorPointer , undefined ) ;
635- }
636- } else {
637- visit ( ariaNode , '' , ! ! options . renderCursorPointer , undefined ) ;
627+ // Do not render the root fragment, just its children.
628+ const nodesToRender = ariaSnapshot . root . role === 'fragment' ? ariaSnapshot . root . children : [ ariaSnapshot . root ] ;
629+ for ( const nodeToRender of nodesToRender ) {
630+ if ( typeof nodeToRender === 'string' )
631+ visitText ( nodeToRender , '' ) ;
632+ else
633+ visit ( nodeToRender , '' , ! ! options . renderCursorPointer , undefined ) ;
638634 }
639635 return lines . join ( '\n' ) ;
640636}
@@ -698,5 +694,5 @@ function textContributesInfo(node: AriaNode, text: string): boolean {
698694}
699695
700696function hasPointerCursor ( ariaNode : AriaNode ) : boolean {
701- return ariaNode . box . style ?. cursor === 'pointer' ;
697+ return ariaNode . box . cursor === 'pointer' ;
702698}
0 commit comments