htmlhint
Version:
The Static Code Analysis Tool for your HTML
1,335 lines (1,232 loc) • 100 kB
JavaScript
/*!
* HTMLHint v1.7.1
* https://htmlhint.com
* Built on: 2025-09-16
* Copyright (c) 2025 HTMLHint
* Licensed under MIT License
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.HTMLHint = factory());
})(this, (function () { 'use strict';
function getDefaultExportFromCjs (x) {
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
}
var core$1 = {};
var htmlparser = {};
var hasRequiredHtmlparser;
function requireHtmlparser () {
if (hasRequiredHtmlparser) return htmlparser;
hasRequiredHtmlparser = 1;
Object.defineProperty(htmlparser, "__esModule", { value: true });
class HTMLParser {
constructor() {
this._listeners = {};
this._mapCdataTags = this.makeMap('script,style');
this._arrBlocks = [];
this.lastEvent = null;
}
makeMap(str) {
const obj = {};
const items = str.split(',');
for (let i = 0; i < items.length; i++) {
obj[items[i]] = true;
}
return obj;
}
parse(html) {
const mapCdataTags = this._mapCdataTags;
const regTag = /<(?:\/([^\s>]+)\s*|!--([\s\S]*?)--|!([^>]*?)|([\w\-:]+)((?:\s+[^\s"'>\/=\x00-\x0F\x7F\x80-\x9F]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'>]*))?)*?)\s*(\/?))>/g;
const regAttr = /\s*([^\s"'>\/=\x00-\x0F\x7F\x80-\x9F]+)(?:\s*=\s*(?:(")([^"]*)"|(')([^']*)'|([^\s"'>]*)))?/g;
const regLine = /\r?\n/g;
let match;
let matchIndex;
let lastIndex = 0;
let tagName;
let arrAttrs;
let tagCDATA = null;
let attrsCDATA;
let arrCDATA = [];
let lastCDATAIndex = 0;
let text;
let lastLineIndex = 0;
let line = 1;
const arrBlocks = this._arrBlocks;
this.fire('start', {
pos: 0,
line: 1,
col: 1,
});
const isMapCdataTagsRequired = () => {
const attrType = arrAttrs.find((attr) => attr.name === 'type') || {
value: '',
};
return (mapCdataTags[tagName] &&
attrType.value.indexOf('text/ng-template') === -1);
};
const saveBlock = (type, raw, pos, data) => {
const col = pos - lastLineIndex + 1;
if (data === undefined) {
data = {};
}
data.raw = raw;
data.pos = pos;
data.line = line;
data.col = col;
arrBlocks.push(data);
this.fire(type, data);
while (regLine.exec(raw)) {
line++;
lastLineIndex = pos + regLine.lastIndex;
}
};
while ((match = regTag.exec(html))) {
matchIndex = match.index;
if (matchIndex > lastIndex) {
text = html.substring(lastIndex, matchIndex);
if (tagCDATA) {
arrCDATA.push(text);
}
else {
saveBlock('text', text, lastIndex);
}
}
lastIndex = regTag.lastIndex;
if ((tagName = match[1])) {
if (tagCDATA && tagName === tagCDATA) {
text = arrCDATA.join('');
saveBlock('cdata', text, lastCDATAIndex, {
tagName: tagCDATA,
attrs: attrsCDATA,
});
tagCDATA = null;
attrsCDATA = undefined;
arrCDATA = [];
}
if (!tagCDATA) {
saveBlock('tagend', match[0], matchIndex, {
tagName: tagName,
});
continue;
}
}
if (tagCDATA) {
arrCDATA.push(match[0]);
}
else {
if ((tagName = match[4])) {
arrAttrs = [];
const attrs = match[5];
let attrMatch;
let attrMatchCount = 0;
while ((attrMatch = regAttr.exec(attrs))) {
const name = attrMatch[1];
const quote = attrMatch[2]
? attrMatch[2]
: attrMatch[4]
? attrMatch[4]
: '';
const value = attrMatch[3]
? attrMatch[3]
: attrMatch[5]
? attrMatch[5]
: attrMatch[6]
? attrMatch[6]
: '';
arrAttrs.push({
name: name,
value: value,
quote: quote,
index: attrMatch.index,
raw: attrMatch[0],
});
attrMatchCount += attrMatch[0].length;
}
if (attrMatchCount === attrs.length) {
saveBlock('tagstart', match[0], matchIndex, {
tagName: tagName,
attrs: arrAttrs,
close: match[6],
});
if (isMapCdataTagsRequired()) {
tagCDATA = tagName;
attrsCDATA = arrAttrs.concat();
arrCDATA = [];
lastCDATAIndex = lastIndex;
}
}
else {
saveBlock('text', match[0], matchIndex);
}
}
else if (match[2] || match[3]) {
saveBlock('comment', match[0], matchIndex, {
content: match[2] || match[3],
long: match[2] ? true : false,
});
}
}
}
if (html.length > lastIndex) {
text = html.substring(lastIndex, html.length);
saveBlock('text', text, lastIndex);
}
this.fire('end', {
pos: lastIndex,
line: line,
col: html.length - lastLineIndex + 1,
});
}
addListener(types, listener) {
const _listeners = this._listeners;
const arrTypes = types.split(/[,\s]/);
let type;
for (let i = 0, l = arrTypes.length; i < l; i++) {
type = arrTypes[i];
if (_listeners[type] === undefined) {
_listeners[type] = [];
}
_listeners[type].push(listener);
}
}
fire(type, data) {
if (data === undefined) {
data = {};
}
data.type = type;
let listeners = [];
const listenersType = this._listeners[type];
const listenersAll = this._listeners['all'];
if (listenersType !== undefined) {
listeners = listeners.concat(listenersType);
}
if (listenersAll !== undefined) {
listeners = listeners.concat(listenersAll);
}
const lastEvent = this.lastEvent;
if (lastEvent !== null) {
delete lastEvent['lastEvent'];
data.lastEvent = lastEvent;
}
this.lastEvent = data;
for (let i = 0, l = listeners.length; i < l; i++) {
listeners[i].call(this, data);
}
}
removeListener(type, listener) {
const listenersType = this._listeners[type];
if (listenersType !== undefined) {
for (let i = 0, l = listenersType.length; i < l; i++) {
if (listenersType[i] === listener) {
listenersType.splice(i, 1);
break;
}
}
}
}
fixPos(event, index) {
const text = event.raw.substr(0, index);
const arrLines = text.split(/\r?\n/);
const lineCount = arrLines.length - 1;
let line = event.line;
let col;
if (lineCount > 0) {
line += lineCount;
col = arrLines[lineCount].length + 1;
}
else {
col = event.col + index;
}
return {
line: line,
col: col,
};
}
getMapAttrs(arrAttrs) {
const mapAttrs = {};
let attr;
for (let i = 0, l = arrAttrs.length; i < l; i++) {
attr = arrAttrs[i];
mapAttrs[attr.name] = attr.value;
}
return mapAttrs;
}
}
htmlparser.default = HTMLParser;
return htmlparser;
}
var reporter = {};
var hasRequiredReporter;
function requireReporter () {
if (hasRequiredReporter) return reporter;
hasRequiredReporter = 1;
Object.defineProperty(reporter, "__esModule", { value: true });
class Reporter {
constructor(html, ruleset) {
this.html = html;
this.lines = html.split(/\r?\n/);
const match = /\r?\n/.exec(html);
this.brLen = match !== null ? match[0].length : 0;
this.ruleset = ruleset;
this.messages = [];
}
info(message, line, col, rule, raw) {
this.report("info", message, line, col, rule, raw);
}
warn(message, line, col, rule, raw) {
this.report("warning", message, line, col, rule, raw);
}
error(message, line, col, rule, raw) {
this.report("error", message, line, col, rule, raw);
}
report(type, message, line, col, rule, raw) {
const lines = this.lines;
const brLen = this.brLen;
let evidence = '';
let evidenceLen = 0;
for (let i = line - 1, lineCount = lines.length; i < lineCount; i++) {
evidence = lines[i];
evidenceLen = evidence.length;
if (col > evidenceLen && line < lineCount) {
line++;
col -= evidenceLen;
if (col !== 1) {
col -= brLen;
}
}
else {
break;
}
}
this.messages.push({
type: type,
message: message,
raw: raw,
evidence: evidence,
line: line,
col: col,
rule: {
id: rule.id,
description: rule.description,
link: `https://htmlhint.com/rules/${rule.id}`,
},
});
}
}
reporter.default = Reporter;
return reporter;
}
var rules = {};
var altRequire = {};
var hasRequiredAltRequire;
function requireAltRequire () {
if (hasRequiredAltRequire) return altRequire;
hasRequiredAltRequire = 1;
Object.defineProperty(altRequire, "__esModule", { value: true });
altRequire.default = {
id: 'alt-require',
description: 'The alt attribute of an <img> element must be present and alt attribute of area[href] and input[type=image] must have a value.',
init(parser, reporter) {
parser.addListener('tagstart', (event) => {
const tagName = event.tagName.toLowerCase();
const mapAttrs = parser.getMapAttrs(event.attrs);
const col = event.col + tagName.length + 1;
let selector;
if (tagName === 'img' && !('alt' in mapAttrs)) {
reporter.warn('An alt attribute must be present on <img> elements.', event.line, col, this, event.raw);
}
else if ((tagName === 'area' && 'href' in mapAttrs) ||
(tagName === 'input' && mapAttrs['type'] === 'image')) {
if (!('alt' in mapAttrs) || mapAttrs['alt'] === '') {
selector = tagName === 'area' ? 'area[href]' : 'input[type=image]';
reporter.warn(`The alt attribute of ${selector} must have a value.`, event.line, col, this, event.raw);
}
}
});
},
};
return altRequire;
}
var attrLowercase = {};
var hasRequiredAttrLowercase;
function requireAttrLowercase () {
if (hasRequiredAttrLowercase) return attrLowercase;
hasRequiredAttrLowercase = 1;
Object.defineProperty(attrLowercase, "__esModule", { value: true });
const svgIgnores = [
'allowReorder',
'attributeName',
'attributeType',
'autoReverse',
'baseFrequency',
'baseProfile',
'calcMode',
'clipPath',
'clipPathUnits',
'contentScriptType',
'contentStyleType',
'diffuseConstant',
'edgeMode',
'externalResourcesRequired',
'filterRes',
'filterUnits',
'glyphRef',
'gradientTransform',
'gradientUnits',
'kernelMatrix',
'kernelUnitLength',
'keyPoints',
'keySplines',
'keyTimes',
'lengthAdjust',
'limitingConeAngle',
'markerHeight',
'markerUnits',
'markerWidth',
'maskContentUnits',
'maskUnits',
'numOctaves',
'onBlur',
'onChange',
'onClick',
'onFocus',
'onKeyUp',
'onLoad',
'pathLength',
'patternContentUnits',
'patternTransform',
'patternUnits',
'pointsAtX',
'pointsAtY',
'pointsAtZ',
'preserveAlpha',
'preserveAspectRatio',
'primitiveUnits',
'refX',
'refY',
'repeatCount',
'repeatDur',
'requiredExtensions',
'requiredFeatures',
'specularConstant',
'specularExponent',
'spreadMethod',
'startOffset',
'stdDeviation',
'stitchTiles',
'surfaceScale',
'systemLanguage',
'tableValues',
'targetX',
'targetY',
'textLength',
'viewBox',
'viewTarget',
'xChannelSelector',
'yChannelSelector',
'zoomAndPan',
];
function testAgainstStringOrRegExp(value, comparison) {
if (comparison instanceof RegExp) {
return comparison.test(value)
? { match: value, pattern: comparison }
: false;
}
const firstComparisonChar = comparison[0];
const lastComparisonChar = comparison[comparison.length - 1];
const secondToLastComparisonChar = comparison[comparison.length - 2];
const comparisonIsRegex = firstComparisonChar === '/' &&
(lastComparisonChar === '/' ||
(secondToLastComparisonChar === '/' && lastComparisonChar === 'i'));
const hasCaseInsensitiveFlag = comparisonIsRegex && lastComparisonChar === 'i';
if (comparisonIsRegex) {
const valueMatches = hasCaseInsensitiveFlag
? new RegExp(comparison.slice(1, -2), 'i').test(value)
: new RegExp(comparison.slice(1, -1)).test(value);
return valueMatches;
}
return value === comparison;
}
attrLowercase.default = {
id: 'attr-lowercase',
description: 'All attribute names must be in lowercase.',
init(parser, reporter, options) {
const exceptions = (Array.isArray(options) ? options : []).concat(svgIgnores);
parser.addListener('tagstart', (event) => {
const attrs = event.attrs;
let attr;
const col = event.col + event.tagName.length + 1;
for (let i = 0, l = attrs.length; i < l; i++) {
attr = attrs[i];
const attrName = attr.name;
if (!exceptions.find((exp) => testAgainstStringOrRegExp(attrName, exp)) &&
attrName !== attrName.toLowerCase()) {
reporter.error(`The attribute name of [ ${attrName} ] must be in lowercase.`, event.line, col + attr.index, this, attr.raw);
}
}
});
},
};
return attrLowercase;
}
var attrNoDuplication = {};
var hasRequiredAttrNoDuplication;
function requireAttrNoDuplication () {
if (hasRequiredAttrNoDuplication) return attrNoDuplication;
hasRequiredAttrNoDuplication = 1;
Object.defineProperty(attrNoDuplication, "__esModule", { value: true });
attrNoDuplication.default = {
id: 'attr-no-duplication',
description: 'Elements cannot have duplicate attributes.',
init(parser, reporter) {
parser.addListener('tagstart', (event) => {
const attrs = event.attrs;
let attr;
let attrName;
const col = event.col + event.tagName.length + 1;
const mapAttrName = {};
for (let i = 0, l = attrs.length; i < l; i++) {
attr = attrs[i];
attrName = attr.name;
if (mapAttrName[attrName] === true) {
reporter.error(`Duplicate of attribute name [ ${attr.name} ] was found.`, event.line, col + attr.index, this, attr.raw);
}
mapAttrName[attrName] = true;
}
});
},
};
return attrNoDuplication;
}
var attrNoUnnecessaryWhitespace = {};
var hasRequiredAttrNoUnnecessaryWhitespace;
function requireAttrNoUnnecessaryWhitespace () {
if (hasRequiredAttrNoUnnecessaryWhitespace) return attrNoUnnecessaryWhitespace;
hasRequiredAttrNoUnnecessaryWhitespace = 1;
Object.defineProperty(attrNoUnnecessaryWhitespace, "__esModule", { value: true });
attrNoUnnecessaryWhitespace.default = {
id: 'attr-no-unnecessary-whitespace',
description: 'No spaces between attribute names and values.',
init(parser, reporter, options) {
const exceptions = Array.isArray(options) ? options : [];
parser.addListener('tagstart', (event) => {
const attrs = event.attrs;
const col = event.col + event.tagName.length + 1;
for (let i = 0; i < attrs.length; i++) {
if (exceptions.indexOf(attrs[i].name) === -1) {
const match = /(\s*)=(\s*)/.exec(attrs[i].raw.trim());
if (match && (match[1].length !== 0 || match[2].length !== 0)) {
reporter.error(`The attribute '${attrs[i].name}' must not have spaces between the name and value.`, event.line, col + attrs[i].index, this, attrs[i].raw);
}
}
}
});
},
};
return attrNoUnnecessaryWhitespace;
}
var attrValueNoDuplication = {};
var hasRequiredAttrValueNoDuplication;
function requireAttrValueNoDuplication () {
if (hasRequiredAttrValueNoDuplication) return attrValueNoDuplication;
hasRequiredAttrValueNoDuplication = 1;
Object.defineProperty(attrValueNoDuplication, "__esModule", { value: true });
attrValueNoDuplication.default = {
id: 'attr-value-no-duplication',
description: 'Class attributes should not contain duplicate values. Other attributes can be checked via configuration.',
init(parser, reporter, options) {
const defaultAttributesToCheck = ['class'];
const attributesToCheck = Array.isArray(options)
? options
: defaultAttributesToCheck;
parser.addListener('tagstart', (event) => {
const attrs = event.attrs;
let attr;
const col = event.col + event.tagName.length + 1;
for (let i = 0, l = attrs.length; i < l; i++) {
attr = attrs[i];
const attrName = attr.name.toLowerCase();
if (!attributesToCheck.includes(attrName)) {
continue;
}
if (!attr.value || !/\s/.test(attr.value)) {
continue;
}
const values = attr.value.trim().split(/\s+/);
const duplicateMap = {};
for (const value of values) {
if (value && duplicateMap[value] === true) {
reporter.error(`Duplicate value [ ${value} ] was found in attribute [ ${attr.name} ].`, event.line, col + attr.index, this, attr.raw);
break;
}
duplicateMap[value] = true;
}
}
});
},
};
return attrValueNoDuplication;
}
var attrSorted = {};
var hasRequiredAttrSorted;
function requireAttrSorted () {
if (hasRequiredAttrSorted) return attrSorted;
hasRequiredAttrSorted = 1;
Object.defineProperty(attrSorted, "__esModule", { value: true });
attrSorted.default = {
id: 'attr-sorted',
description: 'Attribute tags must be in proper order.',
init(parser, reporter) {
const orderMap = {};
const sortOrder = [
'class',
'id',
'name',
'src',
'for',
'type',
'rel',
'href',
'value',
'title',
'alt',
'role',
];
for (let i = 0; i < sortOrder.length; i++) {
orderMap[sortOrder[i]] = i;
}
parser.addListener('tagstart', (event) => {
const attrs = event.attrs;
const listOfAttributes = [];
for (let i = 0; i < attrs.length; i++) {
listOfAttributes.push(attrs[i].name);
}
const originalAttrs = JSON.stringify(listOfAttributes);
listOfAttributes.sort((a, b) => {
if (orderMap[a] !== undefined) {
if (orderMap[b] !== undefined) {
return orderMap[a] - orderMap[b];
}
return -1;
}
if (a.startsWith('data-')) {
if (b.startsWith('data-')) {
return a.localeCompare(b);
}
return 1;
}
if (orderMap[b] !== undefined) {
return 1;
}
if (b.startsWith('data-')) {
return -1;
}
return a.localeCompare(b);
});
if (originalAttrs !== JSON.stringify(listOfAttributes)) {
reporter.error(`Inaccurate order ${originalAttrs} should be in hierarchy ${JSON.stringify(listOfAttributes)} `, event.line, event.col, this, event.raw);
}
});
},
};
return attrSorted;
}
var attrUnsafeChars = {};
var hasRequiredAttrUnsafeChars;
function requireAttrUnsafeChars () {
if (hasRequiredAttrUnsafeChars) return attrUnsafeChars;
hasRequiredAttrUnsafeChars = 1;
Object.defineProperty(attrUnsafeChars, "__esModule", { value: true });
attrUnsafeChars.default = {
id: 'attr-unsafe-chars',
description: 'Attribute values cannot contain unsafe chars.',
init(parser, reporter) {
parser.addListener('tagstart', (event) => {
const attrs = event.attrs;
let attr;
const col = event.col + event.tagName.length + 1;
const regUnsafe = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/;
let match;
for (let i = 0, l = attrs.length; i < l; i++) {
attr = attrs[i];
match = regUnsafe.exec(attr.value);
if (match !== null) {
const unsafeCode = escape(match[0])
.replace(/%u/, '\\u')
.replace(/%/, '\\x');
reporter.warn(`The value of attribute [ ${attr.name} ] cannot contain an unsafe char [ ${unsafeCode} ].`, event.line, col + attr.index, this, attr.raw);
}
}
});
},
};
return attrUnsafeChars;
}
var attrValueDoubleQuotes = {};
var hasRequiredAttrValueDoubleQuotes;
function requireAttrValueDoubleQuotes () {
if (hasRequiredAttrValueDoubleQuotes) return attrValueDoubleQuotes;
hasRequiredAttrValueDoubleQuotes = 1;
Object.defineProperty(attrValueDoubleQuotes, "__esModule", { value: true });
attrValueDoubleQuotes.default = {
id: 'attr-value-double-quotes',
description: 'Attribute values must be in double quotes.',
init(parser, reporter) {
parser.addListener('tagstart', (event) => {
const attrs = event.attrs;
let attr;
const col = event.col + event.tagName.length + 1;
for (let i = 0, l = attrs.length; i < l; i++) {
attr = attrs[i];
if ((attr.value !== '' && attr.quote !== '"') ||
(attr.value === '' && attr.quote === "'")) {
reporter.error(`The value of attribute [ ${attr.name} ] must be in double quotes.`, event.line, col + attr.index, this, attr.raw);
}
}
});
},
};
return attrValueDoubleQuotes;
}
var attrValueNotEmpty = {};
var hasRequiredAttrValueNotEmpty;
function requireAttrValueNotEmpty () {
if (hasRequiredAttrValueNotEmpty) return attrValueNotEmpty;
hasRequiredAttrValueNotEmpty = 1;
Object.defineProperty(attrValueNotEmpty, "__esModule", { value: true });
attrValueNotEmpty.default = {
id: 'attr-value-not-empty',
description: 'All attributes must have values.',
init(parser, reporter) {
parser.addListener('tagstart', (event) => {
const attrs = event.attrs;
let attr;
const col = event.col + event.tagName.length + 1;
for (let i = 0, l = attrs.length; i < l; i++) {
attr = attrs[i];
if (attr.quote === '' && attr.value === '') {
reporter.warn(`The attribute [ ${attr.name} ] must have a value.`, event.line, col + attr.index, this, attr.raw);
}
}
});
},
};
return attrValueNotEmpty;
}
var attrValueSingleQuotes = {};
var hasRequiredAttrValueSingleQuotes;
function requireAttrValueSingleQuotes () {
if (hasRequiredAttrValueSingleQuotes) return attrValueSingleQuotes;
hasRequiredAttrValueSingleQuotes = 1;
Object.defineProperty(attrValueSingleQuotes, "__esModule", { value: true });
attrValueSingleQuotes.default = {
id: 'attr-value-single-quotes',
description: 'Attribute values must be in single quotes.',
init(parser, reporter) {
parser.addListener('tagstart', (event) => {
const attrs = event.attrs;
let attr;
const col = event.col + event.tagName.length + 1;
for (let i = 0, l = attrs.length; i < l; i++) {
attr = attrs[i];
if ((attr.value !== '' && attr.quote !== "'") ||
(attr.value === '' && attr.quote === '"')) {
reporter.error(`The value of attribute [ ${attr.name} ] must be in single quotes.`, event.line, col + attr.index, this, attr.raw);
}
}
});
},
};
return attrValueSingleQuotes;
}
var attrWhitespace = {};
var hasRequiredAttrWhitespace;
function requireAttrWhitespace () {
if (hasRequiredAttrWhitespace) return attrWhitespace;
hasRequiredAttrWhitespace = 1;
Object.defineProperty(attrWhitespace, "__esModule", { value: true });
attrWhitespace.default = {
id: 'attr-whitespace',
description: 'All attributes should be separated by only one space and not have leading/trailing whitespace.',
init(parser, reporter, options) {
const exceptions = Array.isArray(options)
? options
: [];
parser.addListener('tagstart', (event) => {
const attrs = event.attrs;
let attr;
const col = event.col + event.tagName.length + 1;
attrs.forEach((elem) => {
attr = elem;
const attrName = elem.name;
if (exceptions.indexOf(attrName) !== -1) {
return;
}
if (elem.value.trim() !== elem.value) {
reporter.error(`The attributes of [ ${attrName} ] must not have leading or trailing whitespace.`, event.line, col + attr.index, this, attr.raw);
}
if (elem.value.replace(/ +(?= )/g, '') !== elem.value) {
reporter.error(`The attributes of [ ${attrName} ] must be separated by only one space.`, event.line, col + attr.index, this, attr.raw);
}
});
});
},
};
return attrWhitespace;
}
var buttonTypeRequire = {};
var hasRequiredButtonTypeRequire;
function requireButtonTypeRequire () {
if (hasRequiredButtonTypeRequire) return buttonTypeRequire;
hasRequiredButtonTypeRequire = 1;
Object.defineProperty(buttonTypeRequire, "__esModule", { value: true });
buttonTypeRequire.default = {
id: 'button-type-require',
description: 'The type attribute of a <button> element must be present with a valid value: "button", "submit", or "reset".',
init(parser, reporter) {
parser.addListener('tagstart', (event) => {
const tagName = event.tagName.toLowerCase();
if (tagName === 'button') {
const mapAttrs = parser.getMapAttrs(event.attrs);
const col = event.col + tagName.length + 1;
if (mapAttrs.type === undefined) {
reporter.warn('The type attribute must be present on <button> elements.', event.line, col, this, event.raw);
}
else {
const typeValue = mapAttrs.type.toLowerCase();
if (typeValue !== 'button' &&
typeValue !== 'submit' &&
typeValue !== 'reset') {
reporter.warn('The type attribute of <button> must have a valid value: "button", "submit", or "reset".', event.line, col, this, event.raw);
}
}
}
});
},
};
return buttonTypeRequire;
}
var doctypeFirst = {};
var hasRequiredDoctypeFirst;
function requireDoctypeFirst () {
if (hasRequiredDoctypeFirst) return doctypeFirst;
hasRequiredDoctypeFirst = 1;
Object.defineProperty(doctypeFirst, "__esModule", { value: true });
doctypeFirst.default = {
id: 'doctype-first',
description: 'Doctype must be declared first (comments and whitespace allowed before DOCTYPE).',
init(parser, reporter) {
let doctypeFound = false;
let nonCommentContentBeforeDoctype = false;
const allEvent = (event) => {
if (event.type === 'start' ||
(event.type === 'text' && /^\s*$/.test(event.raw))) {
return;
}
if (doctypeFound) {
return;
}
if (event.type === 'comment' &&
event.long === false &&
/^DOCTYPE\s+/i.test(event.content)) {
doctypeFound = true;
if (nonCommentContentBeforeDoctype) {
reporter.error('Doctype must be declared before any non-comment content.', event.line, event.col, this, event.raw);
}
return;
}
if (event.type === 'comment') {
return;
}
nonCommentContentBeforeDoctype = true;
reporter.error('Doctype must be declared before any non-comment content.', event.line, event.col, this, event.raw);
parser.removeListener('all', allEvent);
};
parser.addListener('all', allEvent);
},
};
return doctypeFirst;
}
var doctypeHtml5 = {};
var hasRequiredDoctypeHtml5;
function requireDoctypeHtml5 () {
if (hasRequiredDoctypeHtml5) return doctypeHtml5;
hasRequiredDoctypeHtml5 = 1;
Object.defineProperty(doctypeHtml5, "__esModule", { value: true });
doctypeHtml5.default = {
id: 'doctype-html5',
description: 'Invalid doctype. Use: "<!DOCTYPE html>"',
init(parser, reporter) {
const onComment = (event) => {
if (event.long === false &&
event.content.toLowerCase() !== 'doctype html') {
reporter.warn('Invalid doctype. Use: "<!DOCTYPE html>"', event.line, event.col, this, event.raw);
}
};
const onTagStart = () => {
parser.removeListener('comment', onComment);
parser.removeListener('tagstart', onTagStart);
};
parser.addListener('all', onComment);
parser.addListener('tagstart', onTagStart);
},
};
return doctypeHtml5;
}
var emptyTagNotSelfClosed = {};
var hasRequiredEmptyTagNotSelfClosed;
function requireEmptyTagNotSelfClosed () {
if (hasRequiredEmptyTagNotSelfClosed) return emptyTagNotSelfClosed;
hasRequiredEmptyTagNotSelfClosed = 1;
Object.defineProperty(emptyTagNotSelfClosed, "__esModule", { value: true });
emptyTagNotSelfClosed.default = {
id: 'empty-tag-not-self-closed',
description: 'Empty tags must not use self closed syntax.',
init(parser, reporter) {
const mapEmptyTags = parser.makeMap('area,base,basefont,bgsound,br,col,frame,hr,img,input,isindex,link,meta,param,embed,track,command,source,keygen,wbr');
parser.addListener('tagstart', (event) => {
const tagName = event.tagName.toLowerCase();
if (mapEmptyTags[tagName] !== undefined) {
if (event.close) {
reporter.error(`The empty tag : [ ${tagName} ] must not use self closed syntax.`, event.line, event.col, this, event.raw);
}
}
});
},
};
return emptyTagNotSelfClosed;
}
var formMethodRequire = {};
var hasRequiredFormMethodRequire;
function requireFormMethodRequire () {
if (hasRequiredFormMethodRequire) return formMethodRequire;
hasRequiredFormMethodRequire = 1;
Object.defineProperty(formMethodRequire, "__esModule", { value: true });
formMethodRequire.default = {
id: 'form-method-require',
description: 'The method attribute of a <form> element must be present with a valid value: "get", "post", or "dialog".',
init(parser, reporter) {
const onTagStart = (event) => {
const tagName = event.tagName.toLowerCase();
if (tagName === 'form') {
const mapAttrs = parser.getMapAttrs(event.attrs);
const col = event.col + tagName.length + 1;
if (mapAttrs.method === undefined) {
reporter.warn('The method attribute must be present on <form> elements.', event.line, col, this, event.raw);
}
else {
const methodValue = mapAttrs.method.toLowerCase();
if (methodValue !== 'get' &&
methodValue !== 'post' &&
methodValue !== 'dialog') {
reporter.warn('The method attribute of <form> must have a valid value: "get", "post", or "dialog".', event.line, col, this, event.raw);
}
}
}
};
parser.addListener('tagstart', onTagStart);
},
};
return formMethodRequire;
}
var frameTitleRequire = {};
var hasRequiredFrameTitleRequire;
function requireFrameTitleRequire () {
if (hasRequiredFrameTitleRequire) return frameTitleRequire;
hasRequiredFrameTitleRequire = 1;
Object.defineProperty(frameTitleRequire, "__esModule", { value: true });
frameTitleRequire.default = {
id: 'frame-title-require',
description: 'A <frame> or <iframe> element must have an accessible name.',
init(parser, reporter) {
parser.addListener('tagstart', (event) => {
const tagName = event.tagName.toLowerCase();
const mapAttrs = parser.getMapAttrs(event.attrs);
const col = event.col + tagName.length + 1;
if (tagName === 'frame' || tagName === 'iframe') {
const role = mapAttrs['role'];
if (role === 'presentation' || role === 'none') {
return;
}
const hasAriaLabel = 'aria-label' in mapAttrs && mapAttrs['aria-label'].trim() !== '';
const hasAriaLabelledby = 'aria-labelledby' in mapAttrs &&
mapAttrs['aria-labelledby'].trim() !== '';
const hasTitle = 'title' in mapAttrs && mapAttrs['title'].trim() !== '';
if (!hasAriaLabel && !hasAriaLabelledby && !hasTitle) {
reporter.warn(`A <${tagName}> element must have an accessible name.`, event.line, col, this, event.raw);
}
}
});
},
};
return frameTitleRequire;
}
var h1Require = {};
var hasRequiredH1Require;
function requireH1Require () {
if (hasRequiredH1Require) return h1Require;
hasRequiredH1Require = 1;
Object.defineProperty(h1Require, "__esModule", { value: true });
h1Require.default = {
id: 'h1-require',
description: '<h1> must be present in <body> tag and not be empty.',
init(parser, reporter) {
let bodyDepth = 0;
let hasH1InBody = false;
let bodyTagEvent = null;
let currentH1Event = null;
let h1IsEmpty = false;
const onTagStart = (event) => {
const tagName = event.tagName.toLowerCase();
if (tagName === 'body') {
bodyDepth++;
if (bodyDepth === 1) {
hasH1InBody = false;
bodyTagEvent = event;
}
}
else if (tagName === 'h1' && bodyDepth > 0) {
hasH1InBody = true;
currentH1Event = event;
h1IsEmpty = true;
}
};
const onText = (event) => {
if (currentH1Event && h1IsEmpty) {
if (event.raw && !/^\s*$/.test(event.raw)) {
h1IsEmpty = false;
}
}
};
const onTagEnd = (event) => {
const tagName = event.tagName.toLowerCase();
if (tagName === 'h1' && currentH1Event) {
if (h1IsEmpty) {
reporter.warn('<h1> tag must not be empty.', currentH1Event.line, currentH1Event.col, this, currentH1Event.raw);
}
currentH1Event = null;
}
else if (tagName === 'body') {
if (bodyDepth === 1 && !hasH1InBody && bodyTagEvent) {
reporter.warn('<h1> must be present in <body> tag.', bodyTagEvent.line, bodyTagEvent.col, this, bodyTagEvent.raw);
}
bodyDepth--;
if (bodyDepth < 0)
bodyDepth = 0;
}
};
parser.addListener('tagstart', onTagStart);
parser.addListener('tagend', onTagEnd);
parser.addListener('text', onText);
parser.addListener('end', () => {
if (bodyDepth > 0 && !hasH1InBody && bodyTagEvent) {
reporter.warn('<h1> must be present in <body> tag.', bodyTagEvent.line, bodyTagEvent.col, this, bodyTagEvent.raw);
}
});
},
};
return h1Require;
}
var headScriptDisabled = {};
var hasRequiredHeadScriptDisabled;
function requireHeadScriptDisabled () {
if (hasRequiredHeadScriptDisabled) return headScriptDisabled;
hasRequiredHeadScriptDisabled = 1;
Object.defineProperty(headScriptDisabled, "__esModule", { value: true });
headScriptDisabled.default = {
id: 'head-script-disabled',
description: 'The <script> tag cannot be used in a <head> tag.',
init(parser, reporter) {
const reScript = /^(text\/javascript|application\/javascript)$/i;
let isInHead = false;
const onTagStart = (event) => {
const mapAttrs = parser.getMapAttrs(event.attrs);
const type = mapAttrs.type;
const tagName = event.tagName.toLowerCase();
if (tagName === 'head') {
isInHead = true;
}
if (isInHead === true &&
tagName === 'script' &&
(!type || reScript.test(type) === true)) {
reporter.warn('The <script> tag cannot be used in a <head> tag.', event.line, event.col, this, event.raw);
}
};
const onTagEnd = (event) => {
if (event.tagName.toLowerCase() === 'head') {
parser.removeListener('tagstart', onTagStart);
parser.removeListener('tagend', onTagEnd);
}
};
parser.addListener('tagstart', onTagStart);
parser.addListener('tagend', onTagEnd);
},
};
return headScriptDisabled;
}
var hrefAbsOrRel = {};
var hasRequiredHrefAbsOrRel;
function requireHrefAbsOrRel () {
if (hasRequiredHrefAbsOrRel) return hrefAbsOrRel;
hasRequiredHrefAbsOrRel = 1;
Object.defineProperty(hrefAbsOrRel, "__esModule", { value: true });
hrefAbsOrRel.default = {
id: 'href-abs-or-rel',
description: 'An href attribute must be either absolute or relative.',
init(parser, reporter, options) {
const hrefMode = options === 'abs' ? 'absolute' : 'relative';
parser.addListener('tagstart', (event) => {
const attrs = event.attrs;
let attr;
const col = event.col + event.tagName.length + 1;
for (let i = 0, l = attrs.length; i < l; i++) {
attr = attrs[i];
if (attr.name === 'href') {
if ((hrefMode === 'absolute' && /^\w+?:/.test(attr.value) === false) ||
(hrefMode === 'relative' &&
/^https?:\/\//.test(attr.value) === true)) {
reporter.warn(`The value of the href attribute [ ${attr.value} ] must be ${hrefMode}.`, event.line, col + attr.index, this, attr.raw);
}
break;
}
}
});
},
};
return hrefAbsOrRel;
}
var htmlLangRequire = {};
var hasRequiredHtmlLangRequire;
function requireHtmlLangRequire () {
if (hasRequiredHtmlLangRequire) return htmlLangRequire;
hasRequiredHtmlLangRequire = 1;
Object.defineProperty(htmlLangRequire, "__esModule", { value: true });
const regular = '(art-lojban|cel-gaulish|no-bok|no-nyn|zh-guoyu|zh-hakka|zh-min|zh-min-nan|zh-xiang)';
const irregular = '(en-GB-oed|i-ami|i-bnn|i-default|i-enochian|i-hak|i-klingon|i-lux|i-mingo|i-navajo|i-pwn|i-tao|i-tay|i-tsu|sgn-BE-FR|sgn-BE-NL|sgn-CH-DE)';
const grandfathered = `(?<grandfathered>${irregular}|${regular})`;
const privateUse = '(?<privateUse>x(-[A-Za-z0-9]{1,8})+)';
const privateUse2 = '(?<privateUse2>x(-[A-Za-z0-9]{1,8})+)';
const singleton = '[0-9A-WY-Za-wy-z]';
const extension = `(?<extension>${singleton}(-[A-Za-z0-9]{2,8})+)`;
const variant = '(?<variant>[A-Za-z0-9]{5,8}|[0-9][A-Za-z0-9]{3})';
const region = '(?<region>[A-Za-z]{2}|[0-9]{3})';
const script = '(?<script>[A-Za-z]{4})';
const extlang = '(?<extlang>[A-Za-z]{3}(-[A-Za-z]{3}){0,2})';
const language = `(?<language>([A-Za-z]{2,3}(-${extlang})?)|[A-Za-z]{4}|[A-Za-z]{5,8})`;
const langtag = `(${language}(-${script})?` +
`(-${region})?` +
`(-${variant})*` +
`(-${extension})*` +
`(-${privateUse})?` +
')';
const languageTag = `(${grandfathered}|${langtag}|${privateUse2})`;
htmlLangRequire.default = {
id: 'html-lang-require',
description: 'The lang attribute of an <html> element must be present and should be valid.',
init(parser, reporter) {
parser.addListener('tagstart', (event) => {
const tagName = event.tagName.toLowerCase();
const mapAttrs = parser.getMapAttrs(event.attrs);
const col = event.col + tagName.length + 1;
const langValidityPattern = new RegExp(languageTag, 'g');
if (tagName === 'html') {
if ('lang' in mapAttrs) {
if (!mapAttrs['lang']) {
reporter.warn('The lang attribute of <html> element must have a value.', event.line, col, this, event.raw);
}
else if (!langValidityPattern.test(mapAttrs['lang'])) {
reporter.warn('The lang attribute value of <html> element must be a valid BCP47.', event.line, col, this, event.raw);
}
}
else {
reporter.warn('An lang attribute must be present on <html> elements.', event.line, col, this, event.raw);
}
}
});
},
};
return htmlLangRequire;
}
var idClassAdDisabled = {};
var hasRequiredIdClassAdDisabled;
function requireIdClassAdDisabled () {
if (hasRequiredIdClassAdDisabled) return idClassAdDisabled;
hasRequiredIdClassAdDisabled = 1;
Object.defineProperty(idClassAdDisabled, "__esModule", { value: true });
idClassAdDisabled.default = {
id: 'id-class-ad-disabled',
description: 'The id and class attributes cannot use the ad keyword, it will be blocked by adblock software.',
init(parser, reporter) {
parser.addListener('tagstart', (event) => {
const attrs = event.attrs;
let attr;
let attrName;
const col = event.col + event.tagName.length + 1;
for (let i = 0, l = attrs.length; i < l; i++) {
attr = attrs[i];
attrName = attr.name;
if (/^(id|class)$/i.test(attrName)) {
if (/(^|[-_])ad([-_]|$)/i.test(attr.value)) {
reporter.warn(`The value of attribute ${attrName} cannot use the ad keyword.`, event.line, col + attr.index, this, attr.raw);
}
}
}
});
},
};
return idClassAdDisabled;
}
var idClassValue = {};
var hasRequiredIdClassValue;
function requireIdClassValue () {
if (hasRequiredIdClassValue) return idClassValue;
hasRequiredIdClassValue = 1;
Object.defineProperty(idClassValue, "__esModule", { value: true });
idClassValue.default = {
id: 'id-class-value',
description: 'The id and class attribute values must meet the specified rules.',
init(parser, reporter, options) {
const arrRules = {
underline: {
regId: /^[a-z\d]+(_[a-z\d]+)*$/,
message: 'The id and class attribute values must be in lowercase and split by an underscore.',
},
dash: {
regId: /^[a-z\d]+(-[a-z\d]+)*$/,
message: 'The id and class attribute values must be in lowercase and split by a dash.',
},
hump: {
regId: /^[a-z][a-zA-Z\d]*([A-Z][a-zA-Z\d]*)*$/,
message: 'The id and class attribute values must meet the camelCase style.',
},
};
let rule;
if (typeof options === 'string') {
rule = arrRules[options];
}
else {
rule = options;
}
if (typeof rule === 'object' && rule.regId) {
let regId = rule.regId;
const message = rule.message;
if (!(regId instanceof RegExp)) {
regId = new RegExp(regId);
}
parser.addListener('tagstart', (event) => {
const attrs = event.attrs;
let attr;
const col = event.col + event.tagName.length + 1;
for (let i = 0, l1 = attrs.length; i < l1; i++) {
attr = attrs[i];
if (attr.name.toLowerCase() === 'id') {
if (regId.test(attr.value) === false) {
reporter.warn(message, event.line, col + attr.index, this, attr.raw);
}
}
if (attr.name.toLowerCase() === 'class') {
const arrClass = attr.value.split(/\s+/g);
let classValue;
for (let j = 0, l2 = arrClass.length; j < l2; j++) {
classValue = arrClass[j];