UNPKG

htmlhint

Version:

The Static Code Analysis Tool for your HTML

1,335 lines (1,232 loc) 100 kB
/*! * 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];