1
1
/**
2
- * Returns a then(able) queue of CSSStyleSheet(s)
3
- * @param {Object } ownerDocument document object to be inspected for stylesheets
4
- * @param {number } timeout on network request for stylesheet that need to be externally fetched
5
- * @param {Function } convertTextToStylesheetFn a utility function to generate a style sheet from text
6
- * @return {Object } queue
2
+ * Make an axios get request to fetch a given resource and resolve
3
+ * @method getExternalStylesheet
4
+ * @param {Object } options an object with properties to configure the external XHR
5
+ * @property {Object } options.resolve resolve callback on queue
6
+ * @property {Object } options.reject reject callback on queue
7
+ * @property {String } options.url string representing the url of the resource to load
8
+ * @property {Object } options.rootNode document or shadowDOM root document for which to process CSSOM
9
+ * @property {Number } options.timeout timeout to about network call
10
+ * @property {Function } options.getStyleSheet a utility function to generate a style sheet for a given text content
11
+ * @property {String } options.shadowId an id if undefined denotes that given root is a shadowRoot
12
+ * @property {Number } options.priority css applied priority
13
+ * @returns resolve with stylesheet object
7
14
* @private
8
15
*/
9
- function loadCssom ( { root, shadowId } , timeout , convertTextToStylesheetFn ) {
10
- /**
11
- * Make an axios get request to fetch a given resource and resolve
12
- * @method getExternalStylesheet
13
- * @private
14
- * @param {Object } param an object with properties to configure the external XHR
15
- * @property {Object } param.resolve resolve callback on queue
16
- * @property {Object } param.reject reject callback on queue
17
- * @property {String } param.url string representing the url of the resource to load
18
- * @property {Number } param.timeout timeout to about network call
19
- */
20
- function getExternalStylesheet ( { resolve, reject, url } ) {
21
- axe . imports
22
- . axios ( {
23
- method : 'get' ,
24
- url,
25
- timeout
26
- } )
27
- . then ( ( { data } ) => {
28
- const sheet = convertTextToStylesheetFn ( {
29
- data,
30
- isExternal : true ,
31
- shadowId,
32
- root
33
- } ) ;
34
- resolve ( sheet ) ;
35
- } )
36
- . catch ( reject ) ;
37
- }
38
-
39
- const q = axe . utils . queue ( ) ;
16
+ function getExternalStylesheet ( options ) {
17
+ const {
18
+ resolve,
19
+ reject,
20
+ url,
21
+ rootNode,
22
+ timeout,
23
+ getStyleSheet,
24
+ shadowId,
25
+ priority
26
+ } = options ;
27
+ axe . imports
28
+ . axios ( {
29
+ method : 'get' ,
30
+ url,
31
+ timeout
32
+ } )
33
+ . then ( ( { data } ) => {
34
+ const sheet = getStyleSheet ( {
35
+ data,
36
+ isExternal : true ,
37
+ shadowId,
38
+ root : rootNode ,
39
+ priority
40
+ } ) ;
41
+ resolve ( sheet ) ;
42
+ } )
43
+ . catch ( reject ) ;
44
+ }
40
45
41
- // handle .styleSheets non existent on certain shadowDOM root
42
- const rootStyleSheets = root . styleSheets
43
- ? Array . from ( root . styleSheets )
44
- : null ;
45
- if ( ! rootStyleSheets ) {
46
- return q ;
47
- }
46
+ /**
47
+ * Get stylesheet(s) from shadowDOM
48
+ * @param {Object } documentFragment document fragment node
49
+ * @param {Function } getStyleSheet helper function to get stylesheet object
50
+ * @returns an array of stylesheet objects
51
+ */
52
+ function getSheetsFromShadowDom ( documentFragment , getStyleSheet ) {
53
+ return Array . from ( documentFragment . children ) . reduce ( ( out , node ) => {
54
+ const nodeName = node . nodeName . toUpperCase ( ) ;
55
+ if ( nodeName !== 'STYLE' && nodeName !== 'LINK' ) {
56
+ return out ;
57
+ }
58
+ if ( nodeName === 'STYLE' ) {
59
+ const dynamicSheet = getStyleSheet ( { data : node . textContent } ) ;
60
+ out . push ( dynamicSheet . sheet ) ;
61
+ }
62
+ if ( nodeName === 'LINK' && ! node . media . includes ( 'print' ) ) {
63
+ const dynamicSheet = getStyleSheet ( { data : node , isLink : true } ) ;
64
+ out . push ( dynamicSheet . sheet ) ;
65
+ }
66
+ return out ;
67
+ } , [ ] ) ;
68
+ }
48
69
49
- // convenience array fot help unique sheets if duplicated by same `href`
50
- // both external and internal sheets
70
+ /**
71
+ * Filter a given array of stylesheet objects
72
+ * @param {Array<Object> } styleSheets array of stylesheets
73
+ * @returns an filtered array of stylesheets
74
+ */
75
+ function filterStyleSheets ( styleSheets ) {
51
76
let sheetHrefs = [ ] ;
52
77
53
- // filter out sheets, that should not be accounted for...
54
- const sheets = rootStyleSheets . filter ( sheet => {
55
- // FILTER > sheets with the same href (if exists)
78
+ return styleSheets . filter ( sheet => {
79
+ // 1) FILTER > sheets with the same href
56
80
let sheetAlreadyExists = false ;
57
81
if ( sheet . href ) {
58
82
if ( ! sheetHrefs . includes ( sheet . href ) ) {
@@ -61,81 +85,112 @@ function loadCssom({ root, shadowId }, timeout, convertTextToStylesheetFn) {
61
85
sheetAlreadyExists = true ;
62
86
}
63
87
}
64
- // FILTER > media='print'
65
- // Note:
66
- // Chrome does this automagically, Firefox returns every sheet
67
- // hence the need to filter
88
+ // 2) FILTER > media='print'
68
89
const isPrintMedia = Array . from ( sheet . media ) . includes ( 'print' ) ;
69
- // FILTER > disabled
70
- // Firefox does not respect `disabled` attribute on stylesheet
71
- // Hence decided not to filter out disabled for the time being
72
90
73
- // return
74
91
return ! isPrintMedia && ! sheetAlreadyExists ;
75
92
} ) ;
93
+ }
94
+
95
+ /**
96
+ * Returns a then(able) queue of CSSStyleSheet(s)
97
+ * @method loadCssom
98
+ * @private
99
+ * @param {Object } options an object with attributes essential to load CSSOM
100
+ * @property {Object } options.rootNode document or shadowDOM root document for which to process CSSOM
101
+ * @property {Number } options.rootIndex a number representing the index of the document or shadowDOM, used for priority
102
+ * @property {String } options.shadowId an id if undefined denotes that given root is a shadowRoot
103
+ * @property {Number } options.timeout abort duration for network request
104
+ * @param {Function } options.getStyleSheet a utility function to generate a style sheet for a given text content
105
+ * @return {Object } queue
106
+ */
107
+ function loadCssom ( options ) {
108
+ const { rootNode, rootIndex, shadowId, getStyleSheet } = options ;
109
+ const q = axe . utils . queue ( ) ;
110
+ const styleSheets =
111
+ rootNode . nodeType === 11 && shadowId
112
+ ? getSheetsFromShadowDom ( rootNode , getStyleSheet )
113
+ : Array . from ( rootNode . styleSheets ) ;
114
+ const sheets = filterStyleSheets ( styleSheets ) ;
115
+
116
+ sheets . forEach ( ( sheet , sheetIndex ) => {
117
+ /* eslint max-statements: ["error", 20] */
118
+ const priority = [ rootIndex , sheetIndex ] ;
76
119
77
- // iterate to decipher multi-level nested sheets if any (this is essential to retrieve styles from shadowDOM)
78
- sheets . forEach ( sheet => {
79
- // attempt to retrieve cssRules, or for external sheets make a XMLHttpRequest
80
120
try {
81
- // accessing .cssRules throws for external ( cross-domain) sheets, which is handled in the catch
121
+ // The following line throws an error on cross-origin style sheets:
82
122
const cssRules = sheet . cssRules ;
83
- // read all css rules in the sheet
84
123
const rules = Array . from ( cssRules ) ;
124
+ if ( ! rules . length ) {
125
+ return ;
126
+ }
85
127
86
128
// filter rules that are included by way of @import or nested link
87
129
const importRules = rules . filter ( r => r . href ) ;
88
-
89
- // if no import or nested link rules, with in these cssRules
90
- // return current sheet
91
130
if ( ! importRules . length ) {
92
131
q . defer ( resolve =>
93
132
resolve ( {
94
133
sheet,
95
134
isExternal : false ,
96
135
shadowId,
97
- root
136
+ root : rootNode ,
137
+ priority
98
138
} )
99
139
) ;
100
140
return ;
101
141
}
102
142
103
- // if any import rules exists , fetch via `href` which eventually constructs a sheet with results from resource
143
+ // for import rules, fetch via `href`
104
144
importRules . forEach ( rule => {
105
145
q . defer ( ( resolve , reject ) => {
106
- getExternalStylesheet ( { resolve, reject, url : rule . href } ) ;
146
+ getExternalStylesheet ( {
147
+ resolve,
148
+ reject,
149
+ url : rule . href ,
150
+ priority,
151
+ ...options
152
+ } ) ;
107
153
} ) ;
108
154
} ) ;
109
155
110
156
// in the same sheet - get inline rules in <style> tag or in a CSSStyleSheet excluding @import or nested link
111
157
const inlineRules = rules . filter ( rule => ! rule . href ) ;
158
+ if ( ! inlineRules . length ) {
159
+ return ;
160
+ }
112
161
113
- // concat all cssText into a string for inline rules
162
+ // concat all cssText into a string for inline rules & create sheet
114
163
const inlineRulesCssText = inlineRules
115
164
. reduce ( ( out , rule ) => {
116
165
out . push ( rule . cssText ) ;
117
166
return out ;
118
167
} , [ ] )
119
168
. join ( ) ;
120
- // create and return a sheet with inline rules
121
169
q . defer ( resolve =>
122
170
resolve (
123
- convertTextToStylesheetFn ( {
171
+ getStyleSheet ( {
124
172
data : inlineRulesCssText ,
125
173
shadowId,
126
- root,
127
- isExternal : false
174
+ root : rootNode ,
175
+ isExternal : false ,
176
+ priority
128
177
} )
129
178
)
130
179
) ;
131
180
} catch ( e ) {
132
181
// external sheet -> make an xhr and q the response
133
182
q . defer ( ( resolve , reject ) => {
134
- getExternalStylesheet ( { resolve, reject, url : sheet . href } ) ;
183
+ getExternalStylesheet ( {
184
+ resolve,
185
+ reject,
186
+ url : sheet . href ,
187
+ priority,
188
+ ...options
189
+ } ) ;
135
190
} ) ;
136
191
}
137
- } , [ ] ) ;
138
- // return
192
+ } ) ;
193
+
139
194
return q ;
140
195
}
141
196
@@ -158,7 +213,7 @@ function getAllRootsInTree(tree) {
158
213
. map ( node => {
159
214
return {
160
215
shadowId : node . shadowId ,
161
- root : axe . utils . getRootNode ( node . actualNode )
216
+ rootNode : axe . utils . getRootNode ( node . actualNode )
162
217
} ;
163
218
} ) ;
164
219
return documents ;
@@ -188,34 +243,53 @@ axe.utils.preloadCssom = function preloadCssom({
188
243
189
244
/**
190
245
* Convert text content to CSSStyleSheet
191
- * @method convertTextToStylesheet
246
+ * @method getStyleSheet
192
247
* @private
193
- * @param {Object } param an object with properties to construct stylesheet
194
- * @property {String } param.data text content of the stylesheet
195
- * @property {Boolean } param.isExternal flag to notify if the resource was fetched from the network
196
- * @property {Object } param.doc implementation document to create style elements
197
- * @property {String } param.shadowId (Optional) shadowId if shadowDOM
248
+ * @param {Object } arg an object with properties to construct stylesheet
249
+ * @property {String } arg.data text content of the stylesheet
250
+ * @property {Boolean } arg.isExternal flag to notify if the resource was fetched from the network
251
+ * @property {String } arg.shadowId (Optional) shadowId if shadowDOM
252
+ * @property {Object } arg.root implementation document to create style elements
253
+ * @property {String } arg.priority a number indicating the loaded priority of CSS, to denote specificity of styles contained in the sheet.
198
254
*/
199
- function convertTextToStylesheet ( { data, isExternal, shadowId, root } ) {
255
+ function getStyleSheet ( {
256
+ data,
257
+ isExternal,
258
+ shadowId,
259
+ root,
260
+ priority,
261
+ isLink = false
262
+ } ) {
200
263
const style = dynamicDoc . createElement ( 'style' ) ;
201
- style . type = 'text/css' ;
202
- style . appendChild ( dynamicDoc . createTextNode ( data ) ) ;
264
+ if ( isLink ) {
265
+ // as creating a stylesheet as link will need to be awaited
266
+ // till `onload`, it is wise to convert link href to @import statement
267
+ const text = dynamicDoc . createTextNode ( `@import "${ data . href } "` ) ;
268
+ style . appendChild ( text ) ;
269
+ } else {
270
+ style . appendChild ( dynamicDoc . createTextNode ( data ) ) ;
271
+ }
203
272
dynamicDoc . head . appendChild ( style ) ;
204
273
return {
205
274
sheet : style . sheet ,
206
275
isExternal,
207
276
shadowId,
208
- root
277
+ root,
278
+ priority
209
279
} ;
210
280
}
211
281
212
282
q . defer ( ( resolve , reject ) => {
213
- // as there can be multiple documents (root document, shadow document fragments, and frame documents)
214
- // reduce these into a queue
215
283
roots
216
- . reduce ( ( out , root ) => {
284
+ . reduce ( ( out , root , index ) => {
217
285
out . defer ( ( resolve , reject ) => {
218
- loadCssom ( root , timeout , convertTextToStylesheet )
286
+ loadCssom ( {
287
+ rootNode : root . rootNode ,
288
+ rootIndex : index + 1 , // we want index to start with 1 for priority calculation
289
+ shadowId : root . shadowId ,
290
+ timeout,
291
+ getStyleSheet
292
+ } )
219
293
. then ( resolve )
220
294
. catch ( reject ) ;
221
295
} ) ;
0 commit comments