@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 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: 'a11y',
39
- outputFolder: "public",
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
- // outputFolder must be resolved
31
- if( opts.outputFolder ){
32
- opts.outputFolder = path.join(process.cwd(), opts.outputFolder);
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
- this.config = deepmerge(
35
- DefaultConfig,
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, output } = compiler.options;
50
- let hostUrl = 'localhost' === devServer.host ? `http://${devServer.host}`: devServer.host;
51
- let hostPort = devServer.port;
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
- if( hostPort && 80 !== hostPort )
54
- {
55
- hostUrl = `${hostUrl}:${hostPort}`;
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(`${hostUrl}/${this.config.outputFilename}.html`)
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(`${hostUrl}/${this.config.outputFilename}.html`)
93
+ devServer.open.target.push(`${staticDir.publicPath}/${this.config.outputFilename}.html`)
64
94
  }
65
95
 
66
- // add our static directory
67
- if( Array.isArray(devServer.static) ){
68
- devServer.static.push(staticDir)
69
- }else{
70
- devServer.static = [].concat(devServer.static, staticDir );
71
- }
72
-
73
- // Wait for configuration preset plugins to apply all configure webpack defaults
74
- compiler.hooks.initialize.tap('IBM Accessibility Plugin', () => {
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
- "IBM Accessibility Plugin",
110
+ pluginName,
77
111
  (compilation, { normalModuleFactory }) => {
78
112
  compilation.dependencyFactories.set(
79
113
  EntryDependency,
80
114
  normalModuleFactory
81
115
  );
82
- }
83
- );
84
116
 
85
- const { entry, options, context } = {
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.thisCompilation.tap('IBM Accessibility Plugin',
108
- /**
109
- * Hook into the webpack compilation
110
- * @param {Compilation} compilation
111
- */
112
- (compilation) => {
113
-
114
- compiler.hooks.make.tapAsync("IBM Accessibility Plugin", (compilation, callback) => {
115
-
116
- compilation.addEntry(
117
- context,
118
- dep,
119
- options,
120
- err => {
121
- callback(err);
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
- 'IBM Accessibility Plugin',
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
- let result = this.a11yCheck('auto' === output.publicPath ? output.path : output.publicPath, this.config );
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
- // we have to inject the a11y.update.js file into the head in order for the webpack-dev-server scripts to load.
142
- let pageContent = fs.readFileSync(path.join(staticDir.directory, `${this.config.outputFilename}.html`))
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
- fs.writeFileSync(
145
- path.join(staticDir.directory, `${this.config.outputFilename}.html`),
146
- pageContent.toString().replace('</head>', `<script src="./a11y.update.js"></script>\n</head>`)
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(`${hostUrl}/${this.config.outputFilename}.html`).toString()) }`);
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 = false;
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
- let originalFileName = `${fs.existsSync( url ) ?
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 reportedFile = path.join(outputDir, originalFileName );
240
- let reportedJSon = path.join(outputDir, originalJsonFileName );
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
- // if output file name option was passed
243
- if( outputFilename ){
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
- reportedFile = path.join( outputDir, `${outputFilename}.html` );
246
- reportedJSon = path.join( outputDir, `${outputFilename}.json` );
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
- // rename the output files
249
- fs.renameSync(path.join(outputDir, originalFileName), reportedFile );
250
- fs.renameSync(path.join(outputDir, originalJsonFileName), reportedJSon );
309
+ });
251
310
 
252
- // delete any empty directories.
253
- fs.rmSync( path.join(outputDir, originalFileName.split(path.sep).shift()), {recursive: true} )
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
- return reportedFile;
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.0.9",
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
+ }