-
Notifications
You must be signed in to change notification settings - Fork 246
/
HTMLCS.js
executable file
·630 lines (552 loc) · 20.8 KB
/
HTMLCS.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
/**
* +--------------------------------------------------------------------+
* | This HTML_CodeSniffer file is Copyright (c) |
* | Squiz Pty Ltd (ABN 77 084 670 600) |
* +--------------------------------------------------------------------+
* | IMPORTANT: Your use of this Software is subject to the terms of |
* | the Licence provided in the file licence.txt. If you cannot find |
* | this file please contact Squiz (www.squiz.com.au) so we may |
* | provide you a copy. |
* +--------------------------------------------------------------------+
*
*/
_global.HTMLCS = new function()
{
var _standards = {};
var _sniffs = [];
var _tags = {};
var _standard = null;
var _currentSniff = null;
var _messages = [];
var _msgOverrides = {};
/*
Message type constants.
*/
this.ERROR = 1;
this.WARNING = 2;
this.NOTICE = 3;
// The current language to use.
this.lang = 'en';
/**
* Loads the specified standard and run the sniffs.
*
* @param {String} standard The name of the standard to load.
* @param {String|Node} content An HTML string or a DOM node object.
* @param {Function} callback The function that will be called when the testing is completed.
* @param {Function} failCallback The fail callback which will be called if the standard load has failed.
* @param {String} language The language to use for text output.
*/
this.process = function(
standard,
content,
callback,
failCallback,
language
) {
// Clear previous runs.
_standards = {};
_sniffs = [];
_tags = {};
_standard = null;
if (!content) {
return false;
}
// Set a language to use.
var languages = Object.keys(_global.translation);
if (language && languages.indexOf(language) !== -1) {
this.lang = language;
}
if (_standards[_getStandardPath(standard)]) {
HTMLCS.run(callback, content);
} else {
this.loadStandard(standard, function() {
HTMLCS.run(callback, content);
}, failCallback);
}
};
/**
* Gets a translation for a text value.
*
* @param {String} text The text to get the translation for.
*
* @return {String}
*/
this.getTranslation = function(text) {
var translations = _global.translation[this.lang];
if (!translations) {
console.error('Missing translations for language ' + this.lang);
return '';
}
var translation = translations[text];
if (!translation) {
console.error('Translation for "' + text + '" does not exist in current language ' + this.lang);
return '';
}
return translation;
};
/**
* Loads the specified standard and its sniffs.
*
* @param {String} standard The name of the standard to load.
* @param {Function} callback The function to call once the standard is loaded.
*/
this.loadStandard = function(standard, callback, failCallback) {
if (!standard) {
return false;
}
_includeStandard(standard, function() {
_standard = standard;
callback.call(this);
}, failCallback);
};
/**
* Runs the sniffs for the loaded standard.
*
* @param {Function} callback The function to call once all sniffs are completed.
* @param {String|Node} content An HTML string or a DOM node object.
*/
this.run = function(callback, content) {
var element = null;
var loadingFrame = false;
if (typeof content === 'string') {
loadingFrame = true;
var elementFrame = document.createElement('iframe');
elementFrame.style.display = 'none';
elementFrame = document.body.insertBefore(elementFrame, null);
if (elementFrame.contentDocument) {
element = elementFrame.contentDocument;
} else if (element.contentWindow) {
element = elementFrame.contentWindow.document;
}
elementFrame.load = function() {
this.onreadystatechange = null;
this.onload = null;
if (HTMLCS.isFullDoc(content) === false) {
element = element.getElementsByTagName('body')[0];
var div = element.getElementsByTagName('div')[0];
if (div && (div.id === '__HTMLCS-source-wrap')) {
div.id = '';
element = div;
}
}
var elements = HTMLCS.util.getAllElements(element);
elements.unshift(element);
_run(elements, element, callback);
};
// Satisfy IE which doesn't like onload being set dynamically.
elementFrame.onreadystatechange = function() {
if (/^(complete|loaded)$/.test(this.readyState) === true) {
this.onreadystatechange = null;
this.load();
}
};
elementFrame.onload = elementFrame.load;
if ((HTMLCS.isFullDoc(content) === false) && (content.indexOf('<body') === -1)) {
element.write('<div id="__HTMLCS-source-wrap">' + content + '</div>');
} else {
element.write(content);
}
element.close();
} else {
element = content;
}
if (!element) {
callback.call(this);
return;
}
callback = callback || function() {};
_messages = [];
// Get all the elements in the parent element.
// Add the parent element too, which will trigger "_top" element codes.
var elements = HTMLCS.util.getAllElements(element);
elements.unshift(element);
// Run the sniffs.
if (loadingFrame === false) {
_run(elements, element, callback);
}
};
/**
* Returns true if the content passed appears to be from a full document.
*
* With string content, we consider a full document as the presence of <html>,
* or <head> + <body> elements. For an element, only the 'html' element (the
* document element) is accepted.
*
* @param {String|Node} content An HTML string or a DOM node object.
*
* @returns {Boolean}
*/
this.isFullDoc = function(content) {
var fullDoc = false;
if (typeof content === 'string') {
if (content.toLowerCase().indexOf('<html') !== -1) {
fullDoc = true;
} else if ((content.toLowerCase().indexOf('<head') !== -1) && (content.toLowerCase().indexOf('<body') !== -1)) {
fullDoc = true;
}
} else {
// If we are the document, or the document element.
if ((content.nodeName.toLowerCase() === 'html') || (content.documentElement)) {
fullDoc = true;
}
}
return fullDoc;
};
/**
* Adds a message.
*
* @param {Number} type The type of the message.
* @param {Node} element The element that the message is related to.
* @param {String} msg The message string.
* @param {String} code Unique code for the message.
* @param {Object} [data] Extra data to store for the message.
*/
this.addMessage = function(type, element, msg, code, data) {
code = _getMessageCode(code);
_messages.push({
type: type,
element: element,
msg: _msgOverrides[code] || msg,
code: code,
data: data
});
};
/**
* Returns all the messages for the last run.
*
* Return a copy of the array so the class variable doesn't get modified by
* future modification (eg. splicing).
*
* @returns {Array} Array of message objects.
*/
this.getMessages = function() {
return _messages.concat([]);
};
/**
* Runs the sniffs in the loaded standard for the specified element.
*
* @param {Node} element The element to test.
* @param {Node} topElement The top element of the processing.
* @param {Function} [callback] The function to call once all tests are run.
*/
var _run = function(elements, topElement, callback) {
var topMsgs = [];
while (elements.length > 0) {
var element = elements.shift();
if (element === topElement) {
var tagName = '_top';
} else {
var tagName = element.tagName.toLowerCase();
}
// First check whether any "top" messages need to be shifted off for this
// element. If so, dump off into the main messages.
for (var i = 0; i < topMsgs.length;) {
if (element === topMsgs[i].element) {
_messages.push(topMsgs[i]);
topMsgs.splice(i, 1);
} else {
i++;
}
}//end for
if (_tags[tagName] && _tags[tagName].length > 0) {
_processSniffs(element, _tags[tagName].concat([]), topElement);
// Save "top" messages, and reset the messages array.
if (tagName === '_top') {
topMsgs = _messages;
_messages = [];
}
}
}//end while
_messages = _messages.concat(topMsgs);
// Due to filtering of presentation roles for general sniffing these need to be handled
// separately. The 1.3.1 sniff needs to run to detect any incorrect usage of the presentation
// role.
var presentationElems = topElement.querySelectorAll('[role="presentation"]');
_currentSniff = HTMLCS_WCAG2AAA_Sniffs_Principle1_Guideline1_3_1_3_1;
[].forEach.call(presentationElems, function(element) {
_currentSniff.testSemanticPresentationRole(element);
});
if (callback instanceof Function === true) {
callback.call(this);
}
};
/**
* Process the sniffs.
*
* @param {Node} element The element to test.
* @param {Array} sniffs Array of sniffs.
* @param {Node} topElement The top element of the processing.
* @param {Function} [callback] The function to call once the processing is completed.
*/
var _processSniffs = function(element, sniffs, topElement, callback) {
while (sniffs.length > 0) {
var sniff = sniffs.shift();
_currentSniff = sniff;
if (sniff.useCallback === true) {
// If the useCallback property is set:
// - Process the sniff.
// - Recurse into ourselves with remaining sniffs, with no callback.
// - Clear out the list of sniffs (so they aren't run again), so the
// callback (if not already recursed) can run afterwards.
sniff.process(element, topElement, function() {
_processSniffs(element, sniffs, topElement);
sniffs = [];
});
} else {
// Process the sniff.
sniff.process(element, topElement);
}
}//end while
if (callback instanceof Function === true) {
callback.call(this);
}
};
/**
* Includes the specified standard file.
*
* @param {String} standard The name of the standard.
* @param {Function} callback The function to call once the standard is included.
* @param {Object} options The options for the standard (e.g. exclude sniffs).
*/
var _includeStandard = function(standard, callback, failCallback, options) {
if (standard.indexOf('http') !== 0) {
standard = _getStandardPath(standard);
}//end id
// See if the ruleset object is already included (eg. if minified).
var parts = standard.split('/');
var ruleSet = _global['HTMLCS_' + parts[(parts.length - 2)]];
if (ruleSet) {
// Already included.
_registerStandard(standard, callback, failCallback, options);
} else {
_includeScript(standard, function() {
// Script is included now register the standard.
_registerStandard(standard, callback, failCallback, options);
}, failCallback);
}//end if
};
/**
* Registers the specified standard and its sniffs.
*
* @param {String} standard The name of the standard.
* @param {Function} callback The function to call once the standard is registered.
* @param {Object} options The options for the standard (e.g. exclude sniffs).
*/
var _registerStandard = function(standard, callback, failCallback, options) {
// Get the object name.
var parts = standard.split('/');
// Get a copy of the ruleset object.
var oldRuleSet = _global['HTMLCS_' + parts[(parts.length - 2)]];
var ruleSet = {};
for (var x in oldRuleSet) {
if (oldRuleSet.hasOwnProperty(x) === true) {
ruleSet[x] = oldRuleSet[x];
}
}
if (!ruleSet) {
return false;
}
_standards[standard] = ruleSet;
// Process the options.
if (options) {
if (options.include && options.include.length > 0) {
// Included sniffs.
ruleSet.sniffs = options.include;
} else if (options.exclude) {
// Excluded sniffs.
for (var i = 0; i < options.exclude.length; i++) {
var index = ruleSet.sniffs.find(options.exclude[i]);
if (index >= 0) {
ruleSet.sniffs.splice(index, 1);
}
}
}
}//end if
// Register the sniffs for this standard.
var sniffs = ruleSet.sniffs.slice(0, ruleSet.sniffs.length);
_registerSniffs(standard, sniffs, callback, failCallback);
};
/**
* Registers the sniffs for the specified standard.
*
* @param {String} standard The name of the standard.
* @param {Array} sniffs List of sniffs to register.
* @param {Function} callback The function to call once the sniffs are registered.
*/
var _registerSniffs = function(standard, sniffs, callback, failCallback) {
if (sniffs.length === 0) {
callback.call(this);
return;
}
// Include and register sniffs.
var sniff = sniffs.shift();
_loadSniffFile(standard, sniff, function() {
_registerSniffs(standard, sniffs, callback, failCallback);
}, failCallback);
};
/**
* Includes the sniff's JS file and registers it.
*
* @param {String} standard The name of the standard.
* @param {String|Object} sniff The sniff to register, can be a string or
* and object specifying another standard.
* @param {Function} callback The function to call once the sniff is included and registered.
*/
var _loadSniffFile = function(standard, sniff, callback, failCallback) {
if (typeof sniff === 'string') {
var sniffObj = _getSniff(standard, sniff);
var cb = function() {
_registerSniff(standard, sniff);
callback.call(this);
};
// Already loaded.
if (sniffObj) {
cb();
} else {
_includeScript(_getSniffPath(standard, sniff), cb, failCallback);
}
} else {
// Including a whole other standard.
_includeStandard(sniff.standard, function() {
if (sniff.messages) {
// Add message overrides.
for (var msg in sniff.messages) {
_msgOverrides[msg] = sniff.messages[msg];
}
}
callback.call(this);
}, failCallback, {
exclude: sniff.exclude,
include: sniff.include
});
}
};
/**
* Registers the specified sniff.
*
* @param {String} standard The name of the standard.
* @param {String} sniff The name of the sniff.
*/
var _registerSniff = function(standard, sniff) {
// Get the sniff object.
var sniffObj = _getSniff(standard, sniff);
if (!sniffObj) {
return false;
}
// Call the register method of the sniff, it should return an array of tags.
if (sniffObj.register) {
var watchedTags = sniffObj.register();
for (var i = 0; i < watchedTags.length; i++) {
if (!_tags[watchedTags[i]]) {
_tags[watchedTags[i]] = [];
}
_tags[watchedTags[i]].push(sniffObj);
}
}
_sniffs.push(sniffObj);
};
/**
* Returns the path to the sniff file.
*
* @param {String} standard The name of the standard.
* @param {String} sniff The name of the sniff.
*
* @returns {String} The path to the JS file of the sniff.
*/
var _getSniffPath = function(standard, sniff) {
var parts = standard.split('/');
parts.pop();
var path = parts.join('/') + '/Sniffs/' + sniff.replace(/\./g, '/') + '.js';
return path;
};
/**
* Returns the path to a local standard.
*
* @param {String} standard The name of the standard.
*
* @returns {String} The path to the local standard.
*/
var _getStandardPath = function(standard)
{
// Get the include path of a local standard.
var scripts = document.getElementsByTagName('script');
var path = null;
// Loop through all the script tags that exist in the document and find the one
// that has included this file.
for (var i = 0; i < scripts.length; i++) {
if (scripts[i].src) {
if (scripts[i].src.match(/HTMLCS\.js/)) {
// We have found our appropriate <script> tag that includes
// this file, we can extract the path.
path = scripts[i].src.replace(/HTMLCS\.js/,'');
// trim any trailing bits
path = path.substring(0, path.indexOf('?'));
break;
}
}
}
return path + 'Standards/' + standard + '/ruleset.js';
};
/**
* Returns the sniff object.
*
* @param {String} standard The name of the standard.
* @param {String} sniff The name of the sniff.
*
* @returns {Object} The sniff object.
*/
var _getSniff = function(standard, sniff) {
var name = 'HTMLCS_';
name += _standards[standard].name + '_Sniffs_';
name += sniff.split('.').join('_');
if (!_global[name]) {
return null;
}
_global[name]._name = sniff;
return _global[name];
};
/**
* Returns the full message code.
*
* A full message code includes the standard name, the sniff name and the given code.
*
* @returns {String} The full message code.
*/
var _getMessageCode = function(code) {
code = _standard + '.' + _currentSniff._name + '.' + code;
return code;
};
/**
* Includes the specified JS file.
*
* @param {String} src The URL to the JS file.
* @param {Function} callback The function to call once the script is loaded.
*/
var _includeScript = function(src, callback, failCallback) {
var script = document.createElement('script');
script.onload = function() {
script.onload = null;
script.onreadystatechange = null;
callback.call(this);
};
script.onerror = function() {
script.onload = null;
script.onreadystatechange = null;
if (failCallback) {
failCallback.call(this);
}
};
script.onreadystatechange = function() {
if (/^(complete|loaded)$/.test(this.readyState) === true) {
script.onreadystatechange = null;
script.onload();
}
};
script.src = src;
if (document.head) {
document.head.appendChild(script);
} else {
document.getElementsByTagName('head')[0].appendChild(script);
}
};
};