@@ -18,6 +18,154 @@ function truncate(str, maxLength) {
18
18
return str ;
19
19
}
20
20
21
+ /**
22
+ * Escapes a string for use in CSS selectors
23
+ * @param {String } str - The string to escape
24
+ * @returns {String } The escaped string
25
+ */
26
+ function escapeCSSSelector ( str ) {
27
+ // Use the CSS.escape method if available
28
+ if ( window . CSS && window . CSS . escape ) {
29
+ return window . CSS . escape ( str ) ;
30
+ }
31
+ // Simple fallback for browsers that don't support CSS.escape
32
+ return str
33
+ . replace ( / [ ! " # $ % & ' ( ) * + , . / : ; < = > ? @ [ \\ \] ^ ` { | } ~ ] / g, '\\$&' )
34
+ . replace ( / ^ \d / , '\\3$& ' ) ;
35
+ }
36
+ function generateSelectorWithShadow ( elm ) {
37
+ const selectors = getShadowSelector ( elm ) ;
38
+ if ( typeof selectors === 'string' ) {
39
+ return selectors ;
40
+ } else {
41
+ // merge selectors of an array with ,
42
+ return selectors . join ( ',' ) . replace ( / , $ / , '' ) ;
43
+ }
44
+ }
45
+
46
+ function getShadowSelector ( elm ) {
47
+ if ( ! elm ) {
48
+ return '' ;
49
+ }
50
+ let doc = ( elm . getRootNode && elm . getRootNode ( ) ) || document ;
51
+ // Not a DOCUMENT_FRAGMENT - shadow DOM
52
+ if ( doc . nodeType !== 11 ) {
53
+ return getFullPathSelector ( elm ) ;
54
+ }
55
+
56
+ const stack = [ ] ;
57
+ while ( doc . nodeType === 11 ) {
58
+ if ( ! doc . host ) {
59
+ return '' ;
60
+ }
61
+ stack . unshift ( { elm, doc } ) ;
62
+ elm = doc . host ;
63
+ doc = elm . getRootNode ( ) ;
64
+ }
65
+
66
+ stack . unshift ( { elm, doc } ) ;
67
+ return stack . map ( item => getFullPathSelector ( item . elm ) ) ;
68
+ }
69
+
70
+ function getFullPathSelector ( elm ) {
71
+ if ( elm . nodeName === 'HTML' || elm . nodeName === 'BODY' ) {
72
+ return elm . nodeName . toLowerCase ( ) ;
73
+ }
74
+
75
+ if ( cache . get ( 'getFullPathSelector' ) === undefined ) {
76
+ cache . set ( 'getFullPathSelector' , new WeakMap ( ) ) ;
77
+ }
78
+
79
+ // Check cache first
80
+ const sourceCache = cache . get ( 'getFullPathSelector' ) ;
81
+ if ( sourceCache . has ( elm ) ) {
82
+ return sourceCache . get ( elm ) ;
83
+ }
84
+
85
+ const element = elm ;
86
+ const names = [ ] ;
87
+ while ( elm . parentElement && elm . nodeName !== 'BODY' ) {
88
+ if ( sourceCache . has ( elm ) ) {
89
+ names . unshift ( sourceCache . get ( elm ) ) ;
90
+ break ;
91
+ } else if ( elm . id ) {
92
+ // Check if the ID is unique in the document before using it
93
+ const escapedId = escapeCSSSelector ( elm . getAttribute ( 'id' ) ) ;
94
+ const elementsWithSameId = document . querySelectorAll ( `#${ escapedId } ` ) ;
95
+ if ( elementsWithSameId . length === 1 ) {
96
+ // ID is unique, safe to use
97
+ names . unshift ( '#' + escapedId ) ;
98
+ break ;
99
+ } else {
100
+ // ID is not unique, fallback to position-based selector
101
+ let c = 1 ;
102
+ let e = elm ;
103
+ for ( ; e . previousElementSibling ; e = e . previousElementSibling , c ++ ) {
104
+ // Increment counter for each previous sibling
105
+ }
106
+ names . unshift ( `${ elm . nodeName . toLowerCase ( ) } :nth-child(${ c } )` ) ;
107
+ }
108
+ } else {
109
+ let c = 1 ;
110
+ let e = elm ;
111
+ for ( ; e . previousElementSibling ; e = e . previousElementSibling , c ++ ) {
112
+ // Increment counter for each previous sibling
113
+ }
114
+ names . unshift ( `${ elm . nodeName . toLowerCase ( ) } :nth-child(${ c } )` ) ;
115
+ }
116
+ elm = elm . parentElement ;
117
+ }
118
+
119
+ const selector = names . join ( '>' ) ;
120
+ sourceCache . set ( element , selector ) ;
121
+ return selector ;
122
+ }
123
+
124
+ function getSourceOpt ( element ) {
125
+ if ( ! element ) {
126
+ return '' ;
127
+ }
128
+
129
+ // Initialize cache if needed
130
+ if ( cache . get ( 'getSourceEfficient' ) === undefined ) {
131
+ cache . set ( 'getSourceEfficient' , new WeakMap ( ) ) ;
132
+ }
133
+
134
+ // Check cache first
135
+ const sourceCache = cache . get ( 'getSourceEfficient' ) ;
136
+ if ( sourceCache . has ( element ) ) {
137
+ return sourceCache . get ( element ) ;
138
+ }
139
+
140
+ // Compute value if not cached
141
+ const tagName = element . nodeName ?. toLowerCase ( ) ;
142
+ if ( ! tagName ) {
143
+ return '' ;
144
+ }
145
+
146
+ let result ;
147
+ try {
148
+ const attributes = Array . from ( element . attributes || [ ] )
149
+ . filter ( attr => ! attr . name . startsWith ( 'data-percy-' ) )
150
+ . map ( attr => `${ attr . name } ="${ attr . value } "` )
151
+ . join ( ' ' ) ;
152
+ const closingTag = element . children . length ? false : true ;
153
+ if ( closingTag ) {
154
+ result = `<${ tagName } ${ attributes } >${ element . textContent } </${ tagName } >` ;
155
+ } else {
156
+ result = attributes ? `<${ tagName } ${ attributes } >` : `<${ tagName } >` ;
157
+ }
158
+ result = truncate ( result , 300 ) ; // Truncate to 300 characters
159
+ // Store in cache
160
+ sourceCache . set ( element , result ) ;
161
+ } catch ( e ) {
162
+ // Handle potential errors (like accessing attributes on non-element nodes)
163
+ result = `<${ tagName || 'unknown' } >` ;
164
+ }
165
+
166
+ return result ;
167
+ }
168
+
21
169
function getSource ( element ) {
22
170
if ( ! element ?. outerHTML ) {
23
171
return '' ;
@@ -84,7 +232,11 @@ function DqElement(elm, options = null, spec = {}) {
84
232
this . source = null ;
85
233
// TODO: es-modules_audit
86
234
if ( ! axe . _audit . noHtml ) {
87
- this . source = this . spec . source ?? getSource ( this . _element ) ;
235
+ if ( axe . _cache . get ( 'runTypeAOpt' ) ) {
236
+ this . source = this . spec . source ?? getSourceOpt ( this . _element ) ;
237
+ } else {
238
+ this . source = this . spec . source ?? getSource ( this . _element ) ;
239
+ }
88
240
}
89
241
}
90
242
@@ -94,6 +246,9 @@ DqElement.prototype = {
94
246
* @return {String }
95
247
*/
96
248
get selector ( ) {
249
+ if ( axe . _cache . get ( 'runTypeAOpt' ) ) {
250
+ return this . spec . selector || [ generateSelectorWithShadow ( this . element ) ] ;
251
+ }
97
252
return this . spec . selector || [ getSelector ( this . element , this . _options ) ] ;
98
253
} ,
99
254
0 commit comments