@caweb/a11y-webpack-plugin 1.0.9 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/aceconfig.js +3 -3
- package/index.js +180 -120
- package/package.json +5 -1
- package/reporter.js +390 -0
package/aceconfig.js
CHANGED
|
@@ -35,10 +35,10 @@ export default {
|
|
|
35
35
|
],
|
|
36
36
|
failLevels: failLevels.filter(e=>e),
|
|
37
37
|
reportLevels: reportLevels.filter(e=>e),
|
|
38
|
-
outputFilename: '
|
|
39
|
-
outputFolder: "
|
|
38
|
+
outputFilename: 'reports',
|
|
39
|
+
outputFolder: "/audits/a11y",
|
|
40
40
|
outputFormat: [
|
|
41
|
-
'html'
|
|
41
|
+
'html', 'json'
|
|
42
42
|
],
|
|
43
43
|
outputFilenameTimestamp: false
|
|
44
44
|
}
|
package/index.js
CHANGED
|
@@ -14,6 +14,11 @@ import deepmerge from 'deepmerge';
|
|
|
14
14
|
import chalk from 'chalk';
|
|
15
15
|
import { fileURLToPath, URL } from 'url';
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Internal dependencies
|
|
19
|
+
*/
|
|
20
|
+
import { landingPage, reporter } from './reporter.js';
|
|
21
|
+
|
|
17
22
|
// default configuration
|
|
18
23
|
import {default as DefaultConfig} from './aceconfig.js';
|
|
19
24
|
|
|
@@ -22,138 +27,157 @@ const boldGreen = chalk.bold.green;
|
|
|
22
27
|
const boldBlue = chalk.bold.hex('#03a7fc');
|
|
23
28
|
const currentPath = path.dirname(fileURLToPath(import.meta.url));
|
|
24
29
|
|
|
30
|
+
const pluginName = 'CAWebA11yWebpackPlugin';
|
|
31
|
+
|
|
25
32
|
// IBM Accessibility Checker Plugin
|
|
26
33
|
class A11yPlugin {
|
|
27
34
|
config = {}
|
|
28
35
|
|
|
29
36
|
constructor(opts = {}) {
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
37
|
+
// the default publicPath is always the outputFolder
|
|
38
|
+
DefaultConfig.publicPath = DefaultConfig.outputFolder;
|
|
39
|
+
|
|
40
|
+
// the default output folder is always relative to the current working directory.
|
|
41
|
+
DefaultConfig.outputFolder = path.join( process.cwd(), DefaultConfig.outputFolder );
|
|
42
|
+
|
|
43
|
+
// if opts.outputFolder is defined
|
|
44
|
+
if( opts.outputFolder && ! path.isAbsolute(opts.outputFolder) ){
|
|
45
|
+
opts.publicPath = opts.outputFolder;
|
|
46
|
+
|
|
47
|
+
// we join the current working directory with the opts.outputFolder
|
|
48
|
+
opts.outputFolder = path.join( process.cwd(), opts.outputFolder );
|
|
33
49
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
{
|
|
37
|
-
outputFolder: path.join(currentPath, DefaultConfig.outputFolder)
|
|
38
|
-
},
|
|
39
|
-
opts
|
|
40
|
-
);
|
|
50
|
+
|
|
51
|
+
this.config = deepmerge(DefaultConfig, opts);
|
|
41
52
|
}
|
|
42
53
|
|
|
54
|
+
|
|
43
55
|
apply(compiler) {
|
|
44
56
|
const staticDir = {
|
|
45
57
|
directory: this.config.outputFolder,
|
|
58
|
+
publicPath: encodeURI(this.config.publicPath).replace(':', ''),
|
|
46
59
|
watch: true
|
|
47
60
|
}
|
|
48
61
|
|
|
49
|
-
let { devServer
|
|
50
|
-
let
|
|
51
|
-
let
|
|
62
|
+
let { devServer } = compiler.options;
|
|
63
|
+
let auditUrl = `${devServer.server}://${devServer.host}:${devServer.port}`;
|
|
64
|
+
let nodeModulePath = encodeURI(this.config.publicPath).replace(':', '') + '/node_modules';
|
|
65
|
+
let pathRewrite = {};
|
|
66
|
+
pathRewrite[`^${nodeModulePath}`] = '';
|
|
67
|
+
|
|
68
|
+
let proxy = {
|
|
69
|
+
context: [ nodeModulePath ],
|
|
70
|
+
target: auditUrl,
|
|
71
|
+
pathRewrite,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// we add the proxy to the devServer
|
|
75
|
+
if( Array.isArray(devServer.proxy) ){
|
|
76
|
+
devServer.proxy.push(proxy)
|
|
77
|
+
}else{
|
|
78
|
+
devServer.proxy = [].concat(devServer.proxy, proxy );
|
|
79
|
+
}
|
|
52
80
|
|
|
53
|
-
|
|
54
|
-
{
|
|
55
|
-
|
|
81
|
+
// add our static directory to the devServer
|
|
82
|
+
if( Array.isArray(devServer.static) ){
|
|
83
|
+
devServer.static.push(staticDir)
|
|
84
|
+
}else{
|
|
85
|
+
devServer.static = [].concat(devServer.static, staticDir );
|
|
56
86
|
}
|
|
57
87
|
|
|
58
88
|
// if dev server allows for multiple pages to be opened
|
|
59
89
|
// add outputFilename.html to open property.
|
|
60
90
|
if( Array.isArray(devServer.open) ){
|
|
61
|
-
devServer.open.push(`${
|
|
91
|
+
devServer.open.push(`${staticDir.publicPath}/${this.config.outputFilename}.html`)
|
|
62
92
|
}else if( 'object' === typeof devServer.open && Array.isArray(devServer.open.target) ){
|
|
63
|
-
devServer.open.target.push(`${
|
|
93
|
+
devServer.open.target.push(`${staticDir.publicPath}/${this.config.outputFilename}.html`)
|
|
64
94
|
}
|
|
65
95
|
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
96
|
+
// we always make sure the output folder exists
|
|
97
|
+
fs.mkdirSync( staticDir.directory, { recursive: true } );
|
|
98
|
+
|
|
99
|
+
// Hot Module Replacement
|
|
100
|
+
if( compiler?.options?.devServer?.hot ){
|
|
101
|
+
// we create a blank file for the hot update to compile on our page.
|
|
102
|
+
// this is required for the hot-update to work.
|
|
103
|
+
fs.writeFileSync(
|
|
104
|
+
path.join(staticDir.directory, `a11y.update.js`),
|
|
105
|
+
`` // required for hot-update to compile on our page, blank script for now
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
// we add the entry to the dependency factory during compilation
|
|
75
109
|
compiler.hooks.compilation.tap(
|
|
76
|
-
|
|
110
|
+
pluginName,
|
|
77
111
|
(compilation, { normalModuleFactory }) => {
|
|
78
112
|
compilation.dependencyFactories.set(
|
|
79
113
|
EntryDependency,
|
|
80
114
|
normalModuleFactory
|
|
81
115
|
);
|
|
82
|
-
}
|
|
83
|
-
);
|
|
84
116
|
|
|
85
|
-
|
|
86
|
-
entry: path.join( this.config.outputFolder, 'a11y.update.js'),
|
|
87
|
-
options: {
|
|
88
|
-
name: 'a11y.update'
|
|
89
|
-
},
|
|
90
|
-
context: 'a11y'
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
const dep = new EntryDependency(entry);
|
|
94
|
-
dep.loc = {
|
|
95
|
-
name: options.name
|
|
96
|
-
};
|
|
97
|
-
if( ! fs.existsSync(path.resolve(this.config.outputFolder))){
|
|
98
|
-
fs.mkdirSync( path.resolve(this.config.outputFolder), {recursive: true} );
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
fs.writeFileSync(
|
|
102
|
-
path.join(this.config.outputFolder, `a11y.update.js`),
|
|
103
|
-
`` // required for hot-update to compile on our page, blank script for now
|
|
117
|
+
}
|
|
104
118
|
);
|
|
105
119
|
|
|
106
|
-
|
|
107
|
-
compiler.hooks.
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
120
|
+
// we add the entry before the compilation ends
|
|
121
|
+
compiler.hooks.make.tapAsync(
|
|
122
|
+
pluginName,
|
|
123
|
+
(compilation, callback) => {
|
|
124
|
+
const { entry, options, context } = {
|
|
125
|
+
entry: path.join( staticDir.directory, 'a11y.update.js'),
|
|
126
|
+
options: {
|
|
127
|
+
name: 'a11y.update'
|
|
128
|
+
},
|
|
129
|
+
context: 'a11y'
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const dep = new EntryDependency(entry);
|
|
133
|
+
dep.loc = {
|
|
134
|
+
name: options.name
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
compilation.addEntry(
|
|
138
|
+
context,
|
|
139
|
+
dep,
|
|
140
|
+
options,
|
|
141
|
+
err => {
|
|
142
|
+
callback(err);
|
|
123
143
|
});
|
|
144
|
+
});
|
|
145
|
+
}
|
|
124
146
|
|
|
125
|
-
|
|
126
|
-
|
|
147
|
+
compiler.hooks.thisCompilation.tap(pluginName, (compilation) => {
|
|
148
|
+
// We can audit the html files now that the compilation is done.
|
|
149
|
+
// we hook into the done hook to run the accessibility checker.
|
|
127
150
|
compiler.hooks.done.tapAsync(
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Hook into the process assets hook
|
|
131
|
-
* @param {any} _
|
|
132
|
-
* @param {(err?: Error) => void} callback
|
|
133
|
-
*/
|
|
151
|
+
pluginName,
|
|
134
152
|
(stats, callback) => {
|
|
135
|
-
|
|
153
|
+
|
|
136
154
|
console.log(`<i> ${boldGreen('[webpack-dev-middleware] Running IBM Accessibility scan...')}`);
|
|
137
155
|
|
|
138
|
-
|
|
156
|
+
// we check the compilers output.publicPath
|
|
157
|
+
// if it is not set, we scan the output.path as the publicPath
|
|
158
|
+
let result = this.a11yCheck(
|
|
159
|
+
! compiler.options.output.publicPath ||
|
|
160
|
+
'auto' === compiler.options.output.publicPath ?
|
|
161
|
+
compiler.options.output.path :
|
|
162
|
+
compiler.options.output.publicPath,
|
|
163
|
+
this.config
|
|
164
|
+
);
|
|
139
165
|
|
|
140
|
-
if( result ){
|
|
141
|
-
|
|
142
|
-
|
|
166
|
+
// if( result ){
|
|
167
|
+
// // we have to inject the a11y.update.js file into the head in order for the webpack-dev-server scripts to load.
|
|
168
|
+
// // let pageContent = fs.readFileSync(path.join(staticDir.directory, `${this.config.outputFilename}.html`))
|
|
143
169
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
170
|
+
// // fs.writeFileSync(
|
|
171
|
+
// // path.join(staticDir.directory, `${this.config.outputFilename}.html`),
|
|
172
|
+
// // pageContent.toString().replace('</head>', `<script src="./a11y.update.js"></script>\n</head>`)
|
|
173
|
+
// // )
|
|
174
|
+
// }
|
|
149
175
|
|
|
150
|
-
console.log(`<i> ${boldGreen('[webpack-dev-middleware] IBM Accessibilty Report can be viewed at')} ${ boldBlue(new URL(`${
|
|
176
|
+
console.log(`<i> ${boldGreen('[webpack-dev-middleware] IBM Accessibilty Report can be viewed at')} ${ boldBlue(new URL(`${auditUrl}/${staticDir.publicPath}/${this.config.outputFilename}.html`).toString()) }`);
|
|
151
177
|
|
|
152
|
-
callback();
|
|
153
178
|
});
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
179
|
+
})
|
|
180
|
+
|
|
157
181
|
}
|
|
158
182
|
|
|
159
183
|
/**
|
|
@@ -184,6 +208,12 @@ class A11yPlugin {
|
|
|
184
208
|
outputFilenameTimestamp
|
|
185
209
|
}){
|
|
186
210
|
|
|
211
|
+
|
|
212
|
+
let htmlOutput = outputFormat && outputFormat.includes('html');
|
|
213
|
+
|
|
214
|
+
// we remove the html output since we generate our own html based on the json output.
|
|
215
|
+
// outputFormat = outputFormat.filter(o => 'html' !== o );
|
|
216
|
+
|
|
187
217
|
let acheckerArgs = [
|
|
188
218
|
'--ruleArchive',
|
|
189
219
|
ruleArchive,
|
|
@@ -202,25 +232,11 @@ class A11yPlugin {
|
|
|
202
232
|
url
|
|
203
233
|
];
|
|
204
234
|
|
|
205
|
-
let isValid =
|
|
206
|
-
|
|
207
|
-
if( fs.existsSync( url ) ){
|
|
208
|
-
if( fs.statSync(url).isDirectory() && path.join( url, 'index.html') ){
|
|
209
|
-
url = path.join( url, 'index.html')
|
|
210
|
-
}
|
|
211
|
-
isValid = true;
|
|
212
|
-
}else{
|
|
213
|
-
isValid = 'localhost' === new URL(url).hostname || isUrl( url )
|
|
214
|
-
}
|
|
235
|
+
let isValid = fs.existsSync( url ) || 'localhost' === new URL(url).hostname || isUrl( url );
|
|
236
|
+
let isDirectory = fs.existsSync( url ) && fs.statSync(url).isDirectory();
|
|
215
237
|
|
|
216
238
|
if( isValid ){
|
|
217
|
-
|
|
218
|
-
path.resolve(url).replace(':', '_') :
|
|
219
|
-
url.replace(/http[s]+:\/\//, '')}.html`;
|
|
220
|
-
let originalJsonFileName = `${fs.existsSync( url ) ?
|
|
221
|
-
path.resolve(url).replace(':', '_') :
|
|
222
|
-
url.replace(/http[s]+:\/\//, '')}.json`;
|
|
223
|
-
|
|
239
|
+
|
|
224
240
|
let outputDir = path.resolve('.', outputFolder );
|
|
225
241
|
|
|
226
242
|
let {stderr, stdout} = spawn.sync(
|
|
@@ -236,27 +252,71 @@ class A11yPlugin {
|
|
|
236
252
|
}
|
|
237
253
|
|
|
238
254
|
if( stdout && stdout.toString()){
|
|
239
|
-
let
|
|
240
|
-
|
|
255
|
+
let auditIndex = [];
|
|
256
|
+
|
|
257
|
+
// we iterate thru the output directory in reverse order,
|
|
258
|
+
// this way we can remove any empty directories at the end, since files are cycled first.
|
|
259
|
+
fs.readdirSync(outputDir, {recursive: true}).reverse().forEach( file => {
|
|
260
|
+
// process each json file file
|
|
261
|
+
if( fs.statSync(path.join(outputDir, file)).isFile() ){
|
|
262
|
+
if ( file.startsWith('summary_') ){
|
|
263
|
+
// remove the summary files
|
|
264
|
+
fs.rmSync( path.join(outputDir, file) )
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
let oldName = file;
|
|
269
|
+
let newName = file;
|
|
270
|
+
|
|
271
|
+
// remove the original output directory from the file name
|
|
272
|
+
newName = isDirectory ? file.replace(url.replace(':', '_'), '') : file;
|
|
273
|
+
|
|
274
|
+
newName = newName.replace(/^\\/, '');
|
|
241
275
|
|
|
242
|
-
|
|
243
|
-
|
|
276
|
+
// for some reason .html files have an extra .html in the name
|
|
277
|
+
newName = newName.endsWith('.html.html') ? newName.replace('.html.html', '.html') : newName;
|
|
278
|
+
|
|
279
|
+
// if the new name is not the same as the old name.
|
|
280
|
+
if( newName !== file ){
|
|
281
|
+
// rename the file
|
|
282
|
+
fs.renameSync(
|
|
283
|
+
path.join(outputDir, oldName ),
|
|
284
|
+
path.join(outputDir, newName) );
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// we add the file to the audit index
|
|
288
|
+
if( ! auditIndex.includes(newName) && newName.endsWith('.html') ){
|
|
289
|
+
auditIndex.push( newName );
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// if we are generating html output, we need to generate the html file.
|
|
293
|
+
if( htmlOutput ){
|
|
294
|
+
// let jsonObj = JSON.parse(fs.readFileSync(path.join(outputDir, newName)));
|
|
244
295
|
|
|
245
|
-
|
|
246
|
-
|
|
296
|
+
// we generate the html file
|
|
297
|
+
// reporter( jsonObj, { outputFolder, outputFilename: newName.replace(/\.json$/, '') } );
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
}else if ( fs.statSync(path.join(outputDir, file)).isDirectory() ){
|
|
301
|
+
// process each directory
|
|
302
|
+
// delete any empty directories.
|
|
303
|
+
if( 0 === fs.readdirSync(path.join(outputDir, file)).length ){
|
|
304
|
+
// remove the directory
|
|
305
|
+
fs.rmSync(path.join(outputDir, file), {recursive: true});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
247
308
|
|
|
248
|
-
|
|
249
|
-
fs.renameSync(path.join(outputDir, originalFileName), reportedFile );
|
|
250
|
-
fs.renameSync(path.join(outputDir, originalJsonFileName), reportedJSon );
|
|
309
|
+
});
|
|
251
310
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
}
|
|
311
|
+
// we generate the landing page.
|
|
312
|
+
landingPage( auditIndex, {outputFolder, outputFilename} );
|
|
255
313
|
|
|
314
|
+
// we generate the .
|
|
256
315
|
if( 'a11y' === process.argv[2] ){
|
|
257
|
-
console.log( reportedFile )
|
|
316
|
+
// console.log( reportedFile )
|
|
258
317
|
}else{
|
|
259
|
-
|
|
318
|
+
return true;
|
|
319
|
+
// return reportedFile;
|
|
260
320
|
}
|
|
261
321
|
}
|
|
262
322
|
}else{
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@caweb/a11y-webpack-plugin",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "CAWebPublishing Webpack Plugin to run Accessibility Scans",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"files": [
|
|
8
8
|
"aceconfig.js",
|
|
9
9
|
"index.js",
|
|
10
|
+
"reporter.js",
|
|
10
11
|
"README.md"
|
|
11
12
|
],
|
|
12
13
|
"scripts": {
|
|
@@ -38,5 +39,8 @@
|
|
|
38
39
|
"devDependencies": {
|
|
39
40
|
"webpack": "^5.96.1",
|
|
40
41
|
"webpack-cli": "^5.1.4"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"@caweb/template": "^1.0.2"
|
|
41
45
|
}
|
|
42
46
|
}
|
package/reporter.js
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External dependencies
|
|
3
|
+
*/
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import HandleBars from 'handlebars';
|
|
7
|
+
import htmlFormat from 'html-format';
|
|
8
|
+
|
|
9
|
+
import endsWith from '@caweb/webpack/helpers/logic/endsWith.js';
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
const templateDir = path.resolve( 'node_modules', '@caweb', 'template' );
|
|
13
|
+
|
|
14
|
+
let templatePartials = {
|
|
15
|
+
'header': 'semantics/header.html',
|
|
16
|
+
'footer': 'semantics/footer.html',
|
|
17
|
+
'utilityHeader': 'semantics/utility-header.html',
|
|
18
|
+
'branding': 'semantics/branding.html',
|
|
19
|
+
'mobileControls': 'semantics/mobile-controls.html',
|
|
20
|
+
'navHeader': 'semantics/nav-header.html',
|
|
21
|
+
'navFooter': 'semantics/nav-footer.html',
|
|
22
|
+
'alert': 'components/alert/alert.html',
|
|
23
|
+
'searchForm': 'forms/search.html'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let sortedReport = {
|
|
27
|
+
errors: [],
|
|
28
|
+
warnings: [],
|
|
29
|
+
info: []
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
let title = `IBM Accessibility Equal Access Toolkit: Accessibility Checker Report`;
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Process data
|
|
37
|
+
*
|
|
38
|
+
* Data Object
|
|
39
|
+
* {
|
|
40
|
+
* functions,
|
|
41
|
+
* options,
|
|
42
|
+
* errors,
|
|
43
|
+
* globals,
|
|
44
|
+
* unused,
|
|
45
|
+
* member,
|
|
46
|
+
* file
|
|
47
|
+
* }
|
|
48
|
+
*/
|
|
49
|
+
function addBreakdown({
|
|
50
|
+
functions,
|
|
51
|
+
options,
|
|
52
|
+
errors,
|
|
53
|
+
implieds,
|
|
54
|
+
globals,
|
|
55
|
+
unused,
|
|
56
|
+
member,
|
|
57
|
+
file
|
|
58
|
+
}){
|
|
59
|
+
let functionList = [];
|
|
60
|
+
let errorList = [];
|
|
61
|
+
let unusedList = [];
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Process function data
|
|
65
|
+
*
|
|
66
|
+
* Function Data Object
|
|
67
|
+
* {
|
|
68
|
+
* name,
|
|
69
|
+
* param,
|
|
70
|
+
* line,
|
|
71
|
+
* character,
|
|
72
|
+
* last,
|
|
73
|
+
* lastcharacter,
|
|
74
|
+
* metrics {
|
|
75
|
+
* complexity,
|
|
76
|
+
* parameters,
|
|
77
|
+
* statements
|
|
78
|
+
* }
|
|
79
|
+
* }
|
|
80
|
+
*/
|
|
81
|
+
if( functions ){
|
|
82
|
+
functions.forEach(({ name, param, line, character, metrics }) => {
|
|
83
|
+
let { complexity, parameters, statements } = metrics;
|
|
84
|
+
|
|
85
|
+
functionList.push(
|
|
86
|
+
'<li>',
|
|
87
|
+
`<p><b>Name:</b> ${name}</p>`,
|
|
88
|
+
param && param.length ? `<p><b>Parameters:</b> ${param}</p>` : '',
|
|
89
|
+
`<p><b>Line:</b> ${line}</p>`,
|
|
90
|
+
`<p><b>Col:</b> ${character}</p>`,
|
|
91
|
+
`<p><b>Metrics:</b></p>`,
|
|
92
|
+
'<ul>',
|
|
93
|
+
`<li><b>Cyclomatic Complexity Number:</b> ${complexity}</li>`,
|
|
94
|
+
`<li><b>Arguments:</b> ${parameters}</li>`,
|
|
95
|
+
`<li><b>Statements:</b> ${statements}</li>`,
|
|
96
|
+
'</ul>',
|
|
97
|
+
'</li>'
|
|
98
|
+
)
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Process error data
|
|
105
|
+
*
|
|
106
|
+
* Error Data Object
|
|
107
|
+
* {
|
|
108
|
+
* id,
|
|
109
|
+
* raw,
|
|
110
|
+
* code,
|
|
111
|
+
* evidence,
|
|
112
|
+
* line,
|
|
113
|
+
* character,
|
|
114
|
+
* scope,
|
|
115
|
+
* a,
|
|
116
|
+
* b,
|
|
117
|
+
* c,
|
|
118
|
+
* d,
|
|
119
|
+
* reason
|
|
120
|
+
* }
|
|
121
|
+
*/
|
|
122
|
+
if( errors ){
|
|
123
|
+
errors.forEach(({reason, evidence, line, character}) => {
|
|
124
|
+
errorList.push(
|
|
125
|
+
'<li>',
|
|
126
|
+
`<p><b>Reason:</b> ${reason}</p>`,
|
|
127
|
+
`<p><b>Evidence:</b> ${evidence}</p>`,
|
|
128
|
+
`<p><b>Line:</b> ${line}</p>`,
|
|
129
|
+
`<p><b>Col:</b> ${character}</p>`,
|
|
130
|
+
'</li>'
|
|
131
|
+
)
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Unused Data
|
|
137
|
+
* {
|
|
138
|
+
* name,
|
|
139
|
+
* line,
|
|
140
|
+
* character
|
|
141
|
+
* }
|
|
142
|
+
*/
|
|
143
|
+
if( unused ){
|
|
144
|
+
unused.forEach(({name, line, character}) => {
|
|
145
|
+
unusedList.push(
|
|
146
|
+
'<li>',
|
|
147
|
+
`<p><b>Name:</b> ${name}</p>`,
|
|
148
|
+
`<p><b>Line:</b> ${line}</p>`,
|
|
149
|
+
`<p><b>Col:</b> ${character}</p>`,
|
|
150
|
+
'</li>'
|
|
151
|
+
)
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return `<section id="${file.replace(/[\\:\.]/g, '-').toLowerCase()}" class="mb-5 border border-2">
|
|
156
|
+
<div class="bg-light p-4"><h4>File: <a href="file://${file}" target="_blank" class="fst-italic fs-md text-break">${file}</a></h4></div>
|
|
157
|
+
<div class="p-4">
|
|
158
|
+
<h5>Functions: <span class="bg-light rounded-circle p-2">${functions.length}</span></h5>
|
|
159
|
+
${ functionList.length ? `<ol>${functionList.join('\n')}</ol>` : ''}
|
|
160
|
+
<h5>Errors: <span class="bg-light rounded-circle p-2">${errors ? errors.length : 0}</span></h5>
|
|
161
|
+
${ errorList.length ? `<ol>${errorList.join('\n')}</ol>` : '' }
|
|
162
|
+
<h5>Unused: <span class="bg-light rounded-circle p-2">${unused ? unused.length : 0}</span></h5>
|
|
163
|
+
${ unusedList.length ? `<ol>${unusedList.join('\n')}</ol>` : '' }
|
|
164
|
+
</div>
|
|
165
|
+
</section>`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function initHandleBars(){
|
|
169
|
+
// Register partials.
|
|
170
|
+
Object.entries(templatePartials).forEach(([p, f]) => HandleBars.registerPartial(p, fs.readFileSync(path.resolve(templateDir, f )).toString() ) );
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
// Register custom helpers.
|
|
174
|
+
HandleBars.registerHelper('endsWith', endsWith )
|
|
175
|
+
|
|
176
|
+
return HandleBars.compile(fs.readFileSync(path.resolve(templateDir, 'patterns', 'index.html')).toString() )
|
|
177
|
+
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* JSHint Reporter
|
|
182
|
+
*
|
|
183
|
+
* @param {*} results
|
|
184
|
+
* @param {*} data
|
|
185
|
+
* @param {*} opts
|
|
186
|
+
*/
|
|
187
|
+
function reporter(data, opts){
|
|
188
|
+
let output = [];
|
|
189
|
+
|
|
190
|
+
let {
|
|
191
|
+
outputFolder,
|
|
192
|
+
outputFilename
|
|
193
|
+
} = opts;
|
|
194
|
+
|
|
195
|
+
let {counts, startScan, URL } = data?.summary;
|
|
196
|
+
|
|
197
|
+
let totalIssues = (counts?.violation || 0) +
|
|
198
|
+
(counts?.potentialviolation || 0) +
|
|
199
|
+
(counts?.recommendation || 0) +
|
|
200
|
+
(counts?.potentialrecommendation || 0) +
|
|
201
|
+
(counts?.manual || 0);
|
|
202
|
+
let totalViolations = counts?.violation || 0;
|
|
203
|
+
let totalReviewsNeeded = counts?.potentialviolation || 0;
|
|
204
|
+
let totalRecommendations = (counts?.recommendation || 0) +
|
|
205
|
+
(counts?.potentialrecommendation || 0) +
|
|
206
|
+
(counts?.manual || 0);
|
|
207
|
+
|
|
208
|
+
// currentStatus is the total number of elements minus
|
|
209
|
+
// the number of elementsViolationReview minus the recommendations
|
|
210
|
+
// all divided by the total number of elements
|
|
211
|
+
let currentStatus = (
|
|
212
|
+
(counts?.elements || 0) -
|
|
213
|
+
( (counts?.elementsViolationReview || 0) - (counts?.recommendation || 0) )
|
|
214
|
+
) / (counts?.elements || 0);
|
|
215
|
+
|
|
216
|
+
let violationIcon = '<span class="ca-gov-icon-close-line text-danger align-bottom mx-2"></span>';
|
|
217
|
+
let reviewIcon = '<span class="ca-gov-icon-warning-triangle text-warning align-bottom mx-2"></span>';
|
|
218
|
+
let recommendationIcon = '<span class="ca-gov-icon-info text-primary align-bottom mx-2"></span>';
|
|
219
|
+
|
|
220
|
+
output.push(
|
|
221
|
+
'<div class="container">', // open container
|
|
222
|
+
'<div class="row">', // open row
|
|
223
|
+
'<div class="col-12">', // open column
|
|
224
|
+
`<h1 class="page-title my-4">${title}</h1>`,
|
|
225
|
+
'</div>', // end col-12
|
|
226
|
+
'</div>', // end row
|
|
227
|
+
'<div class="row">', // open row
|
|
228
|
+
'<div class="col-3">', // open column 3
|
|
229
|
+
`<p>${ new Date(startScan).toLocaleString() }</p>`,
|
|
230
|
+
'<strong>Scanned page:</strong>',
|
|
231
|
+
`<p class="text-break">${ URL }</p>`,
|
|
232
|
+
'</div>', // end col-3
|
|
233
|
+
'<div class="col-9">', // open column 9
|
|
234
|
+
'<div class="d-flex p-4" style="background-color: #e8daff; border: 1px solid #8a3ffc">', // open div
|
|
235
|
+
'<div class="w-25">', // open div
|
|
236
|
+
'<p class="fw-bold">Current status</p>',
|
|
237
|
+
`<strong class="fs-1">${Math.ceil(currentStatus * 100)}%</strong>`,
|
|
238
|
+
'<p>Percentage of elements with no detected violations or items to review</p>',
|
|
239
|
+
'</div>', // end div
|
|
240
|
+
'<div class="ps-4 w-75">', // open div
|
|
241
|
+
'<p>This report summarizes automated tests and is generated by <a href="https://www.ibm.com/able/toolkit/tools/#develop" target="_blank">IBM Equal Access Tools</a>. You have to perform additional manual tests to complete accessibility assessments. Use the <a href="https://ibm.com/able/toolkit" target="_blank">IBM Equal Access Toolkit</a> to guide you.</p>',
|
|
242
|
+
'<p class="mb-0">More resources:</p>',
|
|
243
|
+
'<ul class="list-group list-group-flush">',
|
|
244
|
+
'<li class="list-group-item bg-transparent p-0 border-0"><a href="https://www.ibm.com/able/toolkit/develop/overview/#unit-testing" target="_blank">Quick unit test for developers</a></li>',
|
|
245
|
+
'<li class="list-group-item bg-transparent p-0 border-0"><a href="https://www.ibm.com/able/toolkit/verify/overview" target="_blank">Full accessibility test process</a></li>',
|
|
246
|
+
'</ul>',
|
|
247
|
+
'</div>', // end div
|
|
248
|
+
'</div>', // end div
|
|
249
|
+
'<div class="d-flex my-4">', // open div
|
|
250
|
+
'<div class="flex-grow-1 cursor-pointer border border-2 p-2 me-2">', // open div
|
|
251
|
+
`<strong>Violations${violationIcon}</strong>`,
|
|
252
|
+
`<strong class="fs-1 d-block">${totalViolations}</strong>`,
|
|
253
|
+
'<span>Accessibility failures that need to be corrected</span>',
|
|
254
|
+
'</div>', // end div
|
|
255
|
+
'<div class="flex-grow-1 cursor-pointer border border-2 p-2 me-2">', // open div
|
|
256
|
+
`<strong>Needs review${reviewIcon}</strong>`,
|
|
257
|
+
`<strong class="fs-1 d-block">${totalReviewsNeeded}</strong>`,
|
|
258
|
+
'<span>Issues that may not be a violation; manual review is needed</span>',
|
|
259
|
+
'</div>', // end div
|
|
260
|
+
'<div class="flex-grow-1 cursor-pointer border border-2 p-2 me-2">', // open div
|
|
261
|
+
`<strong>Recommendations${recommendationIcon}</strong>`,
|
|
262
|
+
`<strong class="fs-1 d-block">${totalRecommendations}</strong>`,
|
|
263
|
+
'<span>Opportunities to apply best practices to further improve accessibility</span>',
|
|
264
|
+
'</div>', // end div
|
|
265
|
+
'</div>', // end div
|
|
266
|
+
'<div class="d-flex">', // open div
|
|
267
|
+
// '<select>', // open select
|
|
268
|
+
// '<option value="review"><span class="ca-gov-icon-warning-triangle text-warning align-bottom me-2"></span>Needs review</option>', // option
|
|
269
|
+
// '<option value="recommendations"><span class="ca-gov-icon-info text-primary align-bottom me-2"></span>Recommendations</option>', // option
|
|
270
|
+
// '<option value="rules"><span class="ca-gov-icon-close-line text-danger align-bottom me-2"></span>Violations</option>', // option
|
|
271
|
+
// '</select>', // end select
|
|
272
|
+
`<p class="ms-auto me-2">${violationIcon}${totalViolations}</p>`,
|
|
273
|
+
`<p class="mx-2">${reviewIcon}${totalReviewsNeeded}</p>`,
|
|
274
|
+
`<p class="mx-2">${recommendationIcon}${totalRecommendations}</p>`,
|
|
275
|
+
`<p></p>`,
|
|
276
|
+
`<p class="ms-5">${totalIssues} issues found</p>`,
|
|
277
|
+
'</div>', // end div
|
|
278
|
+
'</div>', // end col-9
|
|
279
|
+
'</div>', // end row
|
|
280
|
+
'<div class="row">', // open row
|
|
281
|
+
'<div class="col-12">', // open column
|
|
282
|
+
'<table class="table">', // open table
|
|
283
|
+
'<thead>', // open thead
|
|
284
|
+
'<th>Issues</th>', // th
|
|
285
|
+
'<th>Element roles</th>', // th
|
|
286
|
+
'<th>Requirements</th>', // th
|
|
287
|
+
'<th>Rules</th>', // th
|
|
288
|
+
'</thead>', // end thead
|
|
289
|
+
'<tbody>', // open tbody
|
|
290
|
+
...data?.results?.map( result => {
|
|
291
|
+
if( 'pass' === result.level ){
|
|
292
|
+
return null; // we skip the passed results
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
let icon = recommendationIcon;
|
|
296
|
+
|
|
297
|
+
if( 'violation' === result.level ){
|
|
298
|
+
icon = violationIcon;
|
|
299
|
+
}else if( 'potentialviolation' === result.level ){
|
|
300
|
+
icon = reviewIcon;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return `<tr><td>${icon}</td><td>${result.path.aria}</td><td>${result.category}</td><td>${result.ruleId}</td></tr>`
|
|
304
|
+
}).filter(Boolean),
|
|
305
|
+
'</tbody>', // end tbody
|
|
306
|
+
'</table>', // end table
|
|
307
|
+
'</div>', // end col-12
|
|
308
|
+
'</div>', // end row
|
|
309
|
+
'</div>', // end container
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
HandleBars.registerPartial('index', output.join('\n') );
|
|
313
|
+
|
|
314
|
+
let template = initHandleBars();
|
|
315
|
+
|
|
316
|
+
fs.mkdirSync( outputFolder, {recursive: true} );
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
// we generate the outputFilename
|
|
320
|
+
fs.writeFileSync(
|
|
321
|
+
path.join(outputFolder, outputFilename),
|
|
322
|
+
htmlFormat(
|
|
323
|
+
template({
|
|
324
|
+
title,
|
|
325
|
+
scheme: 'oceanside',
|
|
326
|
+
assets: [
|
|
327
|
+
fs.existsSync( path.join(outputFolder, 'a11y.update.js' ) ) ?
|
|
328
|
+
'a11y.update.js' : '' // if the hot module update file exists, add it
|
|
329
|
+
].filter(Boolean)
|
|
330
|
+
}),
|
|
331
|
+
" ".repeat(4), 250
|
|
332
|
+
)
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function landingPage(data, opts ){
|
|
337
|
+
let output = [];
|
|
338
|
+
|
|
339
|
+
let {
|
|
340
|
+
outputFolder,
|
|
341
|
+
outputFilename
|
|
342
|
+
} = opts;
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
output.push(
|
|
346
|
+
`<div class="container"><div class="row"><div class="col-12"><h1 class="page-title my-4">${title} for ${process.cwd().split('\\').pop()}</h1></div></div></div>`,
|
|
347
|
+
'<div class="container"><div class="row"><div class="col-12">',
|
|
348
|
+
'<table class="table"><thead><tr><th>Page Auditted</th><th>Audit</th></thead><tbody>',
|
|
349
|
+
...data.sort().map(file => {
|
|
350
|
+
// remove the .json extension from the file name
|
|
351
|
+
file = file.replace(/\.json$/, '');
|
|
352
|
+
|
|
353
|
+
return `<tr><td><a href="/${file}" target="_blank">/${file}</a></td><td><a href="${file}" target="_blank">${file}</a></td></tr>`
|
|
354
|
+
}),
|
|
355
|
+
'</tbody></table>',
|
|
356
|
+
'</div></div></div>'
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
HandleBars.registerPartial('index', output.join('\n') );
|
|
360
|
+
|
|
361
|
+
let template = initHandleBars();
|
|
362
|
+
|
|
363
|
+
fs.mkdirSync( outputFolder, {recursive: true} );
|
|
364
|
+
|
|
365
|
+
// we generate the outputFilename
|
|
366
|
+
fs.writeFileSync(
|
|
367
|
+
path.join(outputFolder, `${outputFilename}.html`),
|
|
368
|
+
htmlFormat(
|
|
369
|
+
template({
|
|
370
|
+
title: `${title} for ${process.cwd().split('\\').pop()}`,
|
|
371
|
+
scheme: 'oceanside',
|
|
372
|
+
assets: [
|
|
373
|
+
fs.existsSync( path.join(outputFolder, 'a11y.update.js' ) ) ?
|
|
374
|
+
'a11y.update.js' : '' // if the hot module update file exists, add it
|
|
375
|
+
].filter(Boolean)
|
|
376
|
+
}),
|
|
377
|
+
" ".repeat(4), 250
|
|
378
|
+
)
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function capitalCase(str){
|
|
384
|
+
return str.charAt(0).toUpperCase() + str.slice(1)
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
export {
|
|
388
|
+
reporter,
|
|
389
|
+
landingPage
|
|
390
|
+
}
|