htmllint = function () {
var linter = htmllint.defaultLinter;
return linter.lint.apply(linter, arguments);
}n/a
Linter = function (rules) {
this.rules = RuleList.fromRuleMap(rules);
this.parser = new Parser();
this.inlineConfig = new InlineConfig();
}n/a
dom_builder = function () {
this.parser = null;
this.attributes = {};
this.attribArr = [];
this.dupes = [];
DomHandler.apply(this, Array.prototype.slice.call(arguments));
}n/a
inline_config = function (newBasis) {
this.indexConfigs = [];
this.current = newBasis ? lodash.cloneDeep(newBasis) : this.current;
basis = newBasis ? lodash.cloneDeep(newBasis) : basis;
this.previous = {};
}n/a
function RuleList() {
this.rulesMap = {};
this.subsMap = {};
}n/a
use = function (plugins) {
plugins.forEach(function (plugin) {
if (lodash.isString(plugin)) {
plugin = require(plugin);
}
htmllint.defaultLinter.use(plugin);
});
}n/a
Linter = function (rules) {
this.rules = RuleList.fromRuleMap(rules);
this.parser = new Parser();
this.inlineConfig = new InlineConfig();
}n/a
getOptions = function (args) {
var optList = Array.prototype.slice.call(args, 1);
optList = lodash.flattenDeep(optList);
optList.unshift('default');
return presets.flattenOpts(optList);
}n/a
lint = function () {
var args = Array.prototype.slice.call(arguments);
var callback =
typeof args[args.length - 1] === 'function' ? args.pop() : null;
var ctx = this;
try {
return fn.apply(this, arguments).nodeify(callback, ctx);
} catch (ex) {
if (callback === null || typeof callback == 'undefined') {
return new Promise(function (resolve, reject) {
reject(ex);
});
} else {
asap(function () {
callback.call(ctx, ex);
})
}
}
}n/a
lintByLine = function (lines, opts) {
return this.rules.getRule('line').lint(lines, opts, this.inlineConfig);
}n/a
lintDom = function (dom, opts) {
return this.rules.getRule('dom').lint(dom, opts, this.inlineConfig);
}n/a
resetRules = function (opts) {
var issues = [];
this.rules.forEach(function (rule) {
if (!rule.end) {
return;
}
var result = rule.end(opts);
if (result) {
issues.push(result);
}
});
return lodash.flattenDeep(issues);
}n/a
setupInlineConfigs = function (dom) {
var comments = [];
var feedComments = function (element) {
if (element.type === 'comment') {
comments.push(element);
this.inlineConfig.feedComment(element);
}
if (element.children && element.children.length > 0) {
element.children.forEach(function (child) {
feedComments(child);
});
}
}.bind(this);
if (dom.length) {
dom.forEach(feedComments);
}
}n/a
setupSubscriptions = function () {
this.rules.forEach(function (rule) {
rule.subscribers = this.rules.getSubscribers(rule.name);
}.bind(this));
}n/a
use = function (plugin) {
if (plugin.rules) {
plugin.rules.forEach(function (rule) {
this.rules.addRule(rule);
}.bind(this));
}
}n/a
applyRules = function (rules, element, opts) {
if (!rules) {
return [];
}
return lodash.flattenDeep(rules.map(function(rule) {
var issues = rule.lint.call(rule, element, opts);
addRuleToIssue(issues, rule.name);
return issues;
}));
}...
Object.keys(as).forEach(function(name) {
var a = as[name];
a.name = name;
var matcher = knife.matchFilter.bind(knife, name);
var s = subs.filter(matcher);
issues = issues.concat(knife.applyRules(s, a, opts));
});
return issues;
};
...lint = function (element, opts) {
var subs = this.subscribers,
as = element.attribs,
issues = [];
Object.keys(as).forEach(function(name) {
var a = as[name];
a.name = name;
var matcher = knife.matchFilter.bind(knife, name);
var s = subs.filter(matcher);
issues = issues.concat(knife.applyRules(s, a, opts));
});
return issues;
}n/a
lint = function (element, opts) {
var bannedAttrs = opts[this.name];
if (!bannedAttrs || !element.attribs) {
return [];
}
var issues = [];
var attrs = element.attribs;
bannedAttrs.forEach(function (name) {
if (attrs.hasOwnProperty(name)) {
issues.push(new Issue('E001',
attrs[name].nameLineCol, { attribute: name }));
}
});
return issues;
}n/a
lint = function (attr, opts) {
var format = opts[this.name];
if (!format) {
return [];
}
var ignore = opts['attr-name-ignore-regex'];
if (ignore !== false && (new RegExp(ignore)).test(attr.name)) { return []; }
var verify = knife.getFormatTest(format);
return verify(attr.name) ? [] :
new Issue('E002', attr.nameLineCol, { format: format });
}n/a
lint = function (element, opts) {
if ((!opts[this.name] || !element.dupes) && opts[this.name] !== 0) {
return [];
}
var isPlus = opts[this.name] === '+0';
var aRowLimit = Math.floor(opts[this.name]);
var numberOfAttrsOnFirstRow = 0;
var maxNumberOfAttrsOnAllRows = -1;
var aNumber = Object.keys(element.attribs).length;
var currentNumberOfAttrsInRow = 0;
var currentRowNumber = aNumber > 0 && /\s*\w+\s*\n\s*/.test(element.open) ? 1 : 0;
var prevRowNumber = currentRowNumber;
var prevLineNumber = -1;
Object.keys(element.attribs).forEach(function(attrName) {
prevRowNumber = currentRowNumber;
if (prevLineNumber !== -1 && prevLineNumber !== element.attribs[attrName].valueLineCol[0]) {
if (maxNumberOfAttrsOnAllRows < currentNumberOfAttrsInRow) {
maxNumberOfAttrsOnAllRows = currentNumberOfAttrsInRow;
}
if (currentRowNumber === 0) {
numberOfAttrsOnFirstRow = currentNumberOfAttrsInRow;
}
currentNumberOfAttrsInRow = 0;
currentRowNumber++;
}
if (prevRowNumber === currentRowNumber) {
currentNumberOfAttrsInRow++;
}
prevLineNumber = element.attribs[attrName].valueLineCol[0];
});
if (maxNumberOfAttrsOnAllRows < currentNumberOfAttrsInRow) {
maxNumberOfAttrsOnAllRows = currentNumberOfAttrsInRow;
}
if (currentRowNumber === 0) {
numberOfAttrsOnFirstRow = currentNumberOfAttrsInRow;
}
if ((numberOfAttrsOnFirstRow > aRowLimit || maxNumberOfAttrsOnAllRows > Math.max(1,aRowLimit)) && !(isPlus && aNumber === 1)) {
return new Issue('E037', element.openLineCol, {
limit: aRowLimit
});
}
return [];
}n/a
lint = function (element, opts) {
if (!opts[this.name] || !element.dupes) {
return [];
}
return element.dupes.map(function (n) {
var a = element.attribs[n];
return new Issue('E003', a.nameLineCol, { attribute: n });
});
}n/a
lint = function (attr, opts) {
var format = opts[this.name];
if (!format) {
return [];
}
return regUnsafe.test(attr.value) ? new Issue('E004', attr.valueLineCol) : [];
}n/a
lint = function (element, opts) {
var order = opts[this.name];
if (!order) {
return [];
}
var attrs = element.attribs,
lastpos = 0,
lastname,
issues = [];
order.forEach(function(name) {
if (!attrs.hasOwnProperty(name)) return;
var a = attrs[name];
var pos = a.nameIndex;
if (pos > lastpos) {
lastpos = pos;
lastname = name;
} else {
issues.push(new Issue('E043', a.nameLineCol,
{ attribute: name, previous: lastname }));
}
});
return issues;
}n/a
inputIndices = function (attributes, openTag, openIndex) {
openTag = openTag.slice(openTag.indexOf(' ')); // Remove tag name
var match;
while (match = attrRegex.exec(openTag)) {
var name = match[2].trim();
if (name && attributes.hasOwnProperty(name)) {
var attr = attributes[name];
if (attr.valueIndex !== undefined
|| (!match[5] && attr.nameIndex !== undefined)) {
continue;
}
var nameIndex = openIndex + match.index + match[1].length;
attr.nameIndex = nameIndex;
attr.rawEqValue = match[3];
attr.rawValue = match[5];
if (match[5]) {
attr.valueIndex = nameIndex
+ match[2].length + match[4].length;
}
}
}
Object.keys(attributes).forEach(function (name) {
var attr = attributes[name];
if (attr.valueIndex === undefined) {
attr.valueIndex = attr.nameIndex;
}
});
}...
ele.open = this.htmlText.slice(ele.openIndex + 1, this.parser.endIndex);
ele.openLineCol = this.lineColFunc(ele.openIndex);
// remove duplicate data
delete ele.lineCol;
ele.attribs = this.attributes;
//ele.attribsArr = this.attribArr;
knife.inputIndices(ele.attribs, ele.open, ele.openIndex);
this.attribArr
.sort(function (a, b) {
return ele.attribs[a].nameIndex - ele.attribs[b].nameIndex;
})
.forEach(function (attrib) {
var a = ele.attribs[attrib];
...parseHtmlAttrs = function (attrs) {
var ret = [],
match;
while (match = attrRegex.exec(attrs)) {
ret.push({
name: match[2],
valueRaw: match[5]
});
}
return ret;
}...
match = line.match(regex.open);
if (!match) {
return;
}
// we know this has 'htmllint' at the beginning, now parse the attribute structure if possible
var keyvals = knife.parseHtmlAttrs(match[1]);
var length = keyvals.length,
workingPairs = [];
for (var i = 0; i < length; i++) {
var r = parsePair(keyvals[i].name, keyvals[i].valueRaw);
if ((typeof r) === 'string') {
...lint = function (attr, opts) {
if (!opts[this.name]) {
return [];
}
var format = formats[opts[this.name]] || formats.quoted,
issues = [];
var v = attr.rawValue;
if (v && !format.test(v)) {
var msgData = {
attribute: attr.name,
format: formatNames[opts[this.name]] || formatNames.quoted
};
return new Issue('E005', attr.valueLineCol, msgData);
}
return [];
}n/a
lint = function (attr, opts) {
if (!opts[this.name]) {
return [];
}
var v = attr.rawEqValue;
if (v ? /^[^'"]+=/.test(v) : !knife.isBooleanAttr(attr.name)) {
return new Issue('E006', attr.valueLineCol);
}
return [];
}n/a
isBooleanAttr = function (name) {
return booleanAttrs.indexOf(name.toLowerCase()) >= 0;
}...
module.exports.lint = function (attr, opts) {
if (!opts[this.name]) {
return [];
}
var v = attr.rawEqValue;
if (v ? /^[^'"]+=/.test(v) : !knife.isBooleanAttr(attr.name)) {
return new Issue('E006', attr.valueLineCol);
}
return [];
};
...lint = function (attr, opts) {
var format = opts['class-style'] || opts['id-class-style'];
var nodup = opts['class-no-dup'];
if (!(format || nodup)) {
return [];
}
var issues = [];
var v = attr.value;
var ignore = opts['id-class-ignore-regex'];
var classes = [];
// Parallel to classes; which classes are ignored
var ignore_class = false;
if (ignore) {
var re = new RegExp('(' + ignore + ')|\\s*$|\\s+', 'g');
var res;
var start = 0;
ignore_class = [false];
while (start < v.length && (res = re.exec(v)) !== null) {
if (res[1] === undefined) {
classes.push(v.slice(start, res.index));
start = re.lastIndex;
ignore_class.push(false);
} else {
ignore_class[ignore_class.length - 1] = true;
}
}
ignore_class.pop();
} else {
classes = v.split(/\s+/);
}
if (format) {
var verify = knife.getFormatTest(format);
classes.map(function(c, i) {
if (!(ignore_class[i] || verify(c))) {
issues.push(new Issue('E011', attr.valueLineCol,
{ format: format, 'class': c }));
}
});
}
if (nodup) {
classes = classes.sort();
for (var i = 0; i < classes.length - 1; i++) {
if (classes[i + 1] === classes[i]) {
issues.push(new Issue('E041', attr.valueLineCol,
{ classes: v }));
}
}
}
return issues;
}n/a
end = function () {
this.passedFirst = false;
}n/a
lint = function (element, opts) {
var option = opts[this.name];
if (!option || this.passedFirst ||
element.type === 'comment' || isWhitespace(element)) {
return [];
}
this.passedFirst = true;
if (element.type === 'directive' &&
element.name.toUpperCase() === '!DOCTYPE') {
return [];
}
// If the option is 'smart', fail only if a head tag is present.
if (option === 'smart' &&
!(element.type === 'tag' &&
element.name.toLowerCase() === 'head')) {
return [];
}
return new Issue('E007', element.openLineCol);
}n/a
lint = function (ele, opts) {
if (!opts[this.name] || ele.type !== 'directive') {
return [];
}
// NOTE: this does not support legacy strings or obsolete permitted doctypes
var doctype = /^!DOCTYPE[ \t\n\f]+html[ \t\n\f]*$/i;
var name = /!doctype/i;
if (name.test(ele.name)) {
if (ele.data && doctype.test(ele.data)) {
return [];
} else {
return new Issue('E008', ele.lineCol);
}
} else {
return [];
}
}n/a
lint = function (dom, opts, inlineConfigs) {
var subs = this.subscribers;
/*
* Reset our inline configuration object to be what opts is.
* Does a deep copy so as to not change opts in the future.
*/
inlineConfigs.reset(opts);
var getIssues = function (element) {
var matcher = knife.matchFilter.bind(knife, element.type);
// fast-forwards inlineConfig.current to whatever it should be at this index.
inlineConfigs.getOptsAtIndex(element.index);
var s = subs.filter(matcher);
var ret = knife.applyRules(s, element, inlineConfigs.current);
if (element.children && element.children.length > 0) {
element.children.forEach(function (child) {
ret = ret.concat(getIssues(child));
});
}
return ret;
};
var issues = dom.length ? dom.map(getIssues) : [];
return lodash.flattenDeep(issues);
}n/a
dom_builder = function () {
this.parser = null;
this.attributes = {};
this.attribArr = [];
this.dupes = [];
DomHandler.apply(this, Array.prototype.slice.call(arguments));
}n/a
function DomHandler(callback, options, elementCB){
if(typeof callback === "object"){
elementCB = options;
options = callback;
callback = null;
} else if(typeof options === "function"){
elementCB = options;
options = defaultOpts;
}
this._callback = callback;
this._options = options || defaultOpts;
this._elementCB = elementCB;
this.dom = [];
this._done = false;
this._tagStack = [];
this._parser = this._parser || null;
}n/a
_addDomElement = function (ele) {
if (!this.parser) {
// TODO: rewrite error msg
throw new Error('stop being a bone head >.<');
}
ele.index = this.parser.startIndex;
if (!this.was_closed) { ele.index -= 3; }
ele.lineCol = this.lineColFunc(ele.index);
DomHandler.prototype._addDomElement.call(this, ele);
}n/a
initialize = function (parser) {
this.parser = parser;
}n/a
onattribute = function (name, value) {
if (!this.attributes[name]) {
this.attributes[name] = {
value: value
};
this.attribArr.push(name);
} else {
this.dupes.push(name);
}
}n/a
onclosetag = function () {
var ele = this._tagStack[this._tagStack.length - 1];
if (ele && !knife.isVoidElement(ele.name)) {
// Mercifully, no whitespace is allowed between < and /
this.was_closed = this.htmlText[this.parser.startIndex + 1] === '/';
ele.close = this.was_closed
? this.htmlText.slice(this.parser.startIndex + 2, this.parser.endIndex)
: '';
ele.closeIndex = this.parser.startIndex;
if (!this.was_closed && ele.closeIndex == ele.openIndex) {
ele.closeIndex += ele.open.length + 1;
}
ele.closeLineCol = this.lineColFunc(ele.closeIndex);
}
DomHandler.prototype.onclosetag.call(this);
}n/a
onerror = function (error) {
// TODO: actually bubble this up or queue errors
throw error;
}n/a
onopentag = function (name, attribs) {
DomHandler.prototype.onopentag.call(this, name, attribs);
var ele = this._tagStack[this._tagStack.length - 1];
ele.openIndex = this.parser.startIndex;
if (!this.was_closed) { ele.openIndex -= 3; }
this.was_closed = true;
ele.open = this.htmlText.slice(ele.openIndex + 1, this.parser.endIndex);
ele.openLineCol = this.lineColFunc(ele.openIndex);
// remove duplicate data
delete ele.lineCol;
ele.attribs = this.attributes;
//ele.attribsArr = this.attribArr;
knife.inputIndices(ele.attribs, ele.open, ele.openIndex);
this.attribArr
.sort(function (a, b) {
return ele.attribs[a].nameIndex - ele.attribs[b].nameIndex;
})
.forEach(function (attrib) {
var a = ele.attribs[attrib];
a.nameLineCol = this.lineColFunc(a.nameIndex);
a.valueLineCol = this.lineColFunc(a.valueIndex);
}, this);
this.attribArr = [];
this.attributes = {};
ele.dupes = this.dupes;
this.dupes = [];
}n/a
onprocessinginstruction = function (name, data) {
// htmlparser2 doesn't normally update the position when processing
// declarations or processing directives (<!doctype ...> or <?...> elements)
this.parser._updatePosition(2);
DomHandler.prototype.onprocessinginstruction.call(this, name, data);
}n/a
start = function (htmlText) {
this.htmlText = htmlText;
this.lineColFunc = knife.getLineColFunc(htmlText);
// When a tag has no close, startIndex is too large by 3 for the
// next calls to onopentag and _addDomElement. Keep track of this.
this.was_closed = true;
}...
#!/usr/bin/env node
var repl = require('repl');
var ctx = repl.start('>> ').context;
var htmllint = require('./');
// export stuff to use in the repl
ctx.htmllint = htmllint;
ctx.lint = function () {
...lint = function (ele, opts) {
if (!opts[this.name]) {
return [];
}
if (ele.name === 'figure') {
// get the children of this figure
var children = ele.children;
// check for a figcaption element
for (var i = 0; i < children.length; i++) {
if (children[i].name === 'figcaption') {
return [];
}
}
} else { // ele.name === 'figcaption'
if (ele.parent && ele.parent.name === 'figure'){
return [];
}
}
//return an issue
return new Issue('E032', ele.openLineCol);
}n/a
end = function () {
this.detectedStyle = null;
}n/a
getTabIndexStyle = function (element) {
var a = element.attribs;
if (a && a.hasOwnProperty('tabindex') && typeof a !== 'undefined') {
return a.tabindex.value > 0;
}
return false;
}...
};
module.exports.lint = function (element, opts) {
if (!opts[this.name] || this.isDisabled(element)) {
return [];
}
var tabIndexStyle = this.getTabIndexStyle(element);
if (this.detectedStyle !== null &&
this.detectedStyle !== tabIndexStyle) {
var msg = tabIndexStyle ? 'remove the tabindex'
: 'add a positive tabindex';
return new Issue('E026', element.openLineCol, {op: msg});
...isDisabled = function (element) {
return element.attribs && element.attribs.hasOwnProperty('disabled');
}...
};
module.exports.end = function () {
this.detectedStyle = null;
};
module.exports.lint = function (element, opts) {
if (!opts[this.name] || this.isDisabled(element)) {
return [];
}
var tabIndexStyle = this.getTabIndexStyle(element);
if (this.detectedStyle !== null &&
this.detectedStyle !== tabIndexStyle) {
...lint = function (element, opts) {
if (!opts[this.name] || this.isDisabled(element)) {
return [];
}
var tabIndexStyle = this.getTabIndexStyle(element);
if (this.detectedStyle !== null &&
this.detectedStyle !== tabIndexStyle) {
var msg = tabIndexStyle ? 'remove the tabindex'
: 'add a positive tabindex';
return new Issue('E026', element.openLineCol, {op: msg});
}
this.detectedStyle = tabIndexStyle;
return [];
}n/a
lint = function (elt, opts) {
if (!opts[this.name]) {
return [];
}
var legal_children = ['base', 'link', 'meta', 'noscript', 'script', 'style', 'template', 'title'];
return elt.children.filter(function (e) {
return e.type === 'tag' && legal_children.indexOf(e.name) < 0;
}).map(function(e) {
return new Issue('E047', e.openLineCol);
});
}n/a
lint = function (element, opts) {
var format = opts[this.name];
if (!format) {
return [];
}
// Should return an issue, since a without href is bad
if (!element.attribs || !element.attribs.hasOwnProperty('href')) {
return [];
}
// Link must be absolute iff specified format is absolute
return ((format === 'absolute') ===
(element.attribs.href.value.search('://') !== -1)) ? [] :
new Issue('E009', element.openLineCol, { format: format });
}n/a
lint = function (elt, opts) {
if (!opts[this.name]) {
return [];
}
var output = [],
has_head = false,
has_body = false;
elt.children.forEach(function (e) {
if (e.type !== 'tag') {
return;
}
// E044: Illegal element
// E045: Duplicated tag
// E046: Head and body tags out of order
var err;
if (e.name === 'head') {
err = has_body ? 'E046' : has_head ? 'E045' : false;
has_head = true;
} else if (e.name === 'body') {
err = has_body ? 'E045' : false;
has_body = true;
} else {
err = 'E044';
}
if (err) {
output.push(new Issue(err, e.openLineCol));
}
});
return output;
}n/a
lint = function (attr, opts) {
if (!opts[this.name]) {
return [];
}
var match = /(^|[^a-zA-Z0-9])ad([^a-zA-Z0-9]|$)/.exec(attr.value);
if (!match) {
return [];
}
return new Issue('E010', attr.valueLineCol);
}n/a
end = function () {
// wipe previous table
this.table = {};
}n/a
lint = function (element, opts) {
if (!opts[this.name]) {
return [];
}
// don't process the element if it doesn't have an id
if (!element.attribs.hasOwnProperty('id')) {
return [];
}
var id = element.attribs.id;
// if we haven't seen the id before, remember it
// and pass the element
if (!this.table.hasOwnProperty(id.value)) {
this.table[id.value] = element;
return [];
}
// element has a duplicate id
return new Issue('E012', id.valueLineCol, { id: id.value });
}n/a
lint = function (attr, opts) {
var format = opts['id-class-style'];
if (!format) {
return [];
}
var v = attr.value;
var ignore = opts['id-class-ignore-regex'];
if (ignore !== false && (new RegExp(ignore)).test(v)) { return []; }
var verify = knife.getFormatTest(format);
if (verify(v)) { return []; }
return new Issue('E011', attr.valueLineCol, { format: format, id: v });
}n/a
lint = function (element, opts) {
var opt = opts[this.name];
if (!opt || knife.hasNonEmptyAttr(element, 'alt', opt === 'allownull')) {
return [];
}
return new Issue('E013', element.openLineCol);
}n/a
lint = function (element, opts) {
if (!opts[this.name] || knife.hasNonEmptyAttr(element, 'src')) {
return [];
}
return new Issue('E014', element.openLineCol);
}n/a
end = function () {
delete this.current;
}n/a
lint = function (line, opts) {
// The indent, that is, the whitespace characters before the first
// non-whitespace character.
var matches = /[^ \t]/.exec(line.line);
var sliceEnd = matches !== null ? matches.index : line.line.length;
var indent = line.line.slice(0, sliceEnd);
// if there are no tabs or spaces on this line, don't bother
if (indent.length === 0) {
return [];
}
var output = [];
var width = opts['indent-width'];
if (width) {
var i, l = 0;
for (i = 0; i < indent.length; i++) {
var c = indent[i];
if (c === ' ') {
l++;
} else {
if (l % width !== 0) { break; }
l = 0;
}
}
if (l % width !== 0 && !(opts['indent-width-cont'] &&
line.line[indent.length] !== '<')) {
output.push(new Issue('E036', [line.row, i - l + 1],
{ width: width }));
}
}
var format = opts['indent-style'];
if (format) {
var space = / /.exec(indent);
var tab = /\t/.exec(indent);
if (!this.current) {
this.current = space ? 'spaces' : 'tabs';
}
// true if we require spaces, false if we require tabs
var type = ((format === 'spaces') ||
(format === 'nonmixed' && this.current === 'spaces'));
var error = type ? tab : space;
if (error) {
output.push(new Issue('E024', [line.row, error.index + 1],
{ type: type ? 'Tabs' : 'Spaces' }));
}
}
return output;
}n/a
applyRules = function (rules, element, opts) {
if (!rules) {
return [];
}
return lodash.flattenDeep(rules.map(function(rule) {
var issues = rule.lint.call(rule, element, opts);
addRuleToIssue(issues, rule.name);
return issues;
}));
}...
Object.keys(as).forEach(function(name) {
var a = as[name];
a.name = name;
var matcher = knife.matchFilter.bind(knife, name);
var s = subs.filter(matcher);
issues = issues.concat(knife.applyRules(s, a, opts));
});
return issues;
};
...checkLangTag = function (l) {
if (!l || l.length === 0) { return 0; }
var n = l.lastIndexOf('-');
var lang = '', country = '';
if (n === -1) { lang = l; }
else { lang = l.slice(0,n); country = l.slice(n + 1, l.length); }
return (checkLang(lang) && checkCountry(country)) ? 0
: (checkLang(lang.toLowerCase()) && checkCountry(country.toUpperCase())) ? 2
: 1;
}...
};
module.exports.lint = function (element, opts) {
var a = element.attribs;
if (a && a.hasOwnProperty('lang')) {
var l = a.lang.value;
if (opts['lang-style']) {
var valid = knife.checkLangTag(l);
if (valid === 1) {
return new Issue('E038', a.lang.valueLineCol, {lang:l});
}
if (opts['lang-style'] === 'case' && valid === 2) {
return new Issue('E039', a.lang.valueLineCol, {lang:l});
}
}
...getFormatTest = function (name) {
var regex = lodash.isRegExp(name) ? name : formats[name];
return regex.test.bind(regex);
}...
if (!format) {
return [];
}
var ignore = opts['attr-name-ignore-regex'];
if (ignore !== false && (new RegExp(ignore)).test(attr.name)) { return []; }
var verify = knife.getFormatTest(format);
return verify(attr.name) ? [] :
new Issue('E002', attr.nameLineCol, { format: format });
};
...getLineColFunc = function (htmlText, lineCol) {
var lastInd = 0,
line = 0,
col = 0;
if (lineCol && lineCol[0] && lineCol[1]) {
line = lineCol[0];
col = lineCol[1];
}
return function (i) {
if (i < lastInd) {
throw new Error('Index passed to line/column' + ' function (' + i + ') does not keep with order (last was ' + lastInd
+ ')');
}
while (lastInd < i) {
if (htmlText[lastInd] === '\n') {
col = 0;
line++;
} else {
col++;
}
lastInd++;
}
return [line + 1, col + 1];
};
}...
// NOTE: this must be called before parsing begins
DomBuilder.prototype.initialize = function (parser) {
this.parser = parser;
};
DomBuilder.prototype.start = function (htmlText) {
this.htmlText = htmlText;
this.lineColFunc = knife.getLineColFunc(htmlText);
// When a tag has no close, startIndex is too large by 3 for the
// next calls to onopentag and _addDomElement. Keep track of this.
this.was_closed = true;
};
DomBuilder.prototype.onerror = function (error) {
// TODO: actually bubble this up or queue errors
...hasNonEmptyAttr = function (tag, attr, allowNull) {
var a = tag.attribs[attr];
return (a && (allowNull || (a.value && a.value.length > 0)));
}...
name: 'img-req-alt',
on: ['tag'],
filter: ['img']
};
module.exports.lint = function (element, opts) {
var opt = opts[this.name];
if (!opt || knife.hasNonEmptyAttr(element, 'alt', opt === 'allownull
')) {
return [];
}
return new Issue('E013', element.openLineCol);
};
...inputIndices = function (attributes, openTag, openIndex) {
openTag = openTag.slice(openTag.indexOf(' ')); // Remove tag name
var match;
while (match = attrRegex.exec(openTag)) {
var name = match[2].trim();
if (name && attributes.hasOwnProperty(name)) {
var attr = attributes[name];
if (attr.valueIndex !== undefined
|| (!match[5] && attr.nameIndex !== undefined)) {
continue;
}
var nameIndex = openIndex + match.index + match[1].length;
attr.nameIndex = nameIndex;
attr.rawEqValue = match[3];
attr.rawValue = match[5];
if (match[5]) {
attr.valueIndex = nameIndex
+ match[2].length + match[4].length;
}
}
}
Object.keys(attributes).forEach(function (name) {
var attr = attributes[name];
if (attr.valueIndex === undefined) {
attr.valueIndex = attr.nameIndex;
}
});
}...
ele.open = this.htmlText.slice(ele.openIndex + 1, this.parser.endIndex);
ele.openLineCol = this.lineColFunc(ele.openIndex);
// remove duplicate data
delete ele.lineCol;
ele.attribs = this.attributes;
//ele.attribsArr = this.attribArr;
knife.inputIndices(ele.attribs, ele.open, ele.openIndex);
this.attribArr
.sort(function (a, b) {
return ele.attribs[a].nameIndex - ele.attribs[b].nameIndex;
})
.forEach(function (attrib) {
var a = ele.attribs[attrib];
...isBooleanAttr = function (name) {
return booleanAttrs.indexOf(name.toLowerCase()) >= 0;
}...
module.exports.lint = function (attr, opts) {
if (!opts[this.name]) {
return [];
}
var v = attr.rawEqValue;
if (v ? /^[^'"]+=/.test(v) : !knife.isBooleanAttr(attr.name)) {
return new Issue('E006', attr.valueLineCol);
}
return [];
};
...isLabeable = function (ele) {
if (ele.type !== 'tag' || !lodash.includes(elems, ele.name)) {
// element isn't a tag or isn't a labeable element
return false;
}
if (ele.name === 'input' && ele.attribs && ele.attribs.type &&
ele.attribs.type.value === 'hidden') {
// inputs that are hidden are not labeable elements
return false;
}
// element passed all the tests
return true;
}...
var id = ele.attribs['for'].value,
forElement = this.idmap[id];
if (!forElement) {
// the paired element does not exist
return new Issue('E021', ele.openLineCol, { id: id });
} else if (!knife.isLabeable(forElement)) {
return new Issue('E022', ele.openLineCol, { id: id });
}
}
return [];
};
...isSelfClosing = function (element) {
var openRaw = element.open;
return openRaw[openRaw.length - 1] === '/';
}...
};
module.exports.lint = function (element, opts) {
// If the tag did not close itself
if (!element.close ||
element.name.toLowerCase() !== element.close.toLowerCase()) {
if (knife.isVoidElement(element.name)) {
var selfClose = knife.isSelfClosing(element);
var style = opts['tag-self-close'];
if ((style == 'always' && !selfClose) ||
(style == 'never' && selfClose)) {
return new Issue('E018', element.openLineCol,
{expect: style});
}
} else {
...isVoidElement = function (tagName) {
return elems.indexOf(tagName) !== -1;
}...
ele.dupes = this.dupes;
this.dupes = [];
};
DomBuilder.prototype.onclosetag = function () {
var ele = this._tagStack[this._tagStack.length - 1];
if (ele && !knife.isVoidElement(ele.name)) {
// Mercifully, no whitespace is allowed between < and /
this.was_closed = this.htmlText[this.parser.startIndex + 1] === '/';
ele.close = this.was_closed
? this.htmlText.slice(this.parser.startIndex + 2, this.parser.endIndex)
: '';
ele.closeIndex = this.parser.startIndex;
if (!this.was_closed && ele.closeIndex == ele.openIndex) {
...matchFilter = function (data, rule) {
if (!rule.filter) {
return true;
}
return rule.filter.indexOf(data.toLowerCase()) > -1;
}n/a
parseHtmlAttrs = function (attrs) {
var ret = [],
match;
while (match = attrRegex.exec(attrs)) {
ret.push({
name: match[2],
valueRaw: match[5]
});
}
return ret;
}...
match = line.match(regex.open);
if (!match) {
return;
}
// we know this has 'htmllint' at the beginning, now parse the attribute structure if possible
var keyvals = knife.parseHtmlAttrs(match[1]);
var length = keyvals.length,
workingPairs = [];
for (var i = 0; i < length; i++) {
var r = parsePair(keyvals[i].name, keyvals[i].valueRaw);
if ((typeof r) === 'string') {
...shred = function (html) {
// Take the HTML string
// Return an array of {line, line number, index}
var row = 1,
ind = 0,
shredded = [];
while (html) {
var len = html.search('[\r\n]') + 1;
if (len === 0) { len = html.length; }
else if (html[len - 1] === '\r' && html[len] === '\n') { len++; }
shredded[row] = {
line: html.substr(0, len),
index: ind,
row: row
};
row++; ind += len;
html = html.slice(len);
}
return shredded;
}n/a
inline_config = function (newBasis) {
this.indexConfigs = [];
this.current = newBasis ? lodash.cloneDeep(newBasis) : this.current;
basis = newBasis ? lodash.cloneDeep(newBasis) : basis;
this.previous = {};
}n/a
addConfig = function (config) {
if (this.indexConfigs[config.end]) {
throw new Error('config exists at index already!');
}
this.indexConfigs[config.end] = config;
}...
var config = {
start: element.index,
end: element.index + element.data.length + 6, // 7 for the '<!--' and '-->', spaces were in
element.data already
rules: workingPairs //in order!
};
// add it
this.addConfig(config);
};
/**
* Accept an attribute and return either a parsed config pair object
* or an error string.
* @param {string} name - The attribute name.
* @param {string} value - The attribute raw value.
...clear = function () {
this.indexConfigs = [];
this.reset(null);
}n/a
feedComment = function (element) {
var line = element.data,
match = line.match(regex.open);
if (!match) {
return;
}
// we know this has 'htmllint' at the beginning, now parse the attribute structure if possible
var keyvals = knife.parseHtmlAttrs(match[1]);
var length = keyvals.length,
workingPairs = [];
for (var i = 0; i < length; i++) {
var r = parsePair(keyvals[i].name, keyvals[i].valueRaw);
if ((typeof r) === 'string') {
throw new Error(r);
} else {
workingPairs.push(r);
}
}
if (workingPairs.length < 1) {
return;
}
var config = {
start: element.index,
end: element.index + element.data.length + 6, // 7 for the '<!--' and '-->', spaces were in element.data already
rules: workingPairs //in order!
};
// add it
this.addConfig(config);
}n/a
getOptsAtIndex = function (newIndex) {
if (newIndex !== 0 && newIndex <= index) {
throw new Error('Cannot get options for index ' + newIndex + ' when index ' + index + ' has already been checked');
} else {
var configs = lodash.compact(this.indexConfigs.slice(index + 1, newIndex + 1));
index = newIndex;
/*
* NOTE: right now, this only allows for a maximum of one config to be applied
* from one call to the next. This makes sense if we call the function on each element or
* even each comment. If this changes later, use a loop below.
*/
// if there are no configs between the previous this.current and the new this.current, do nothing.
if (!configs[0]) {
return false;
}
return applyConfig.call(this, configs[0]); // apply that config
}
}...
*/
inlineConfigs.reset(opts);
var getIssues = function (element) {
var matcher = knife.matchFilter.bind(knife, element.type);
// fast-forwards inlineConfig.current to whatever it should be at this index.
inlineConfigs.getOptsAtIndex(element.index);
var s = subs.filter(matcher);
var ret = knife.applyRules(s, element, inlineConfigs.current);
if (element.children && element.children.length > 0) {
element.children.forEach(function (child) {
ret = ret.concat(getIssues(child));
...reset = function (newBasis) {
basis = newBasis ? lodash.cloneDeep(newBasis) : basis;
this.current = lodash.cloneDeep(basis);
index = 0;
}...
basis = null; // a copy of the original options given to us, for a reset.
/**
* An inline configuration class is created to hold each inline configuration
* and report back what the options should be at a certain index.
* @constructor
* @param {Object} newBasis - The set of options to have at the start (index 0).
* If not given here, it must be set with inlineConfig.reset(basis).
*/
var inlineConfig = function (newBasis) {
this.indexConfigs = [];
this.current = newBasis ? lodash.cloneDeep(newBasis) : this.current;
basis = newBasis ? lodash.cloneDeep(newBasis) : basis;
this.previous = {};
};
...lint = function (element, opts) {
if (!opts[this.name]) {
return [];
}
// if it's not a radio-type input, ignore it
var a = element.attribs;
if (!(a.type && a.type.value === 'radio')) {
return [];
}
if (knife.hasNonEmptyAttr(element, 'name')) {
return [];
}
return new Issue('E034', element.openLineCol);
}n/a
end = function () {
var issues = [];
if (this.inputsInfo.length > 0) {
this.inputsInfo.forEach(function (input) {
if (!this.labels[input.id] && !this.labels[input.name]) {
issues.push(new Issue('E033', input.location, {
'idValue': input.id,
'nameValue': input.name
}));
}
}.bind(this));
}
// wipe previous table
this.labels = {};
this.inputsInfo = [];
return issues;
}n/a
lint = function (element, opts) {
if (!opts[this.name]) {
return [];
}
// if it's a label with a 'for', store that value
if (element.name === 'label') {
if (element.attribs.hasOwnProperty('for')) {
this.labels[element.attribs.for.value] = element;
}
return [];
}
// if it's not a text-type input, ignore it
if (!(element.attribs.hasOwnProperty('type')) || !(element.attribs.type.value === 'text' || element.attribs.type.value === '
radio')) {
return [];
}
var type = element.attribs.type.value;
// check if the input has a label as a parent.
var parent = element.parent;
while (parent !== null) {
if (parent.name === 'label') {
return [];
}
parent = parent.parent;
}
// check if the input has a named label, by storing the values to check at the end.
var id = (element.attribs.hasOwnProperty('id') && element.attribs.id) ? element.attribs.id.value : null;
var name = (element.attribs.hasOwnProperty('name') && element.attribs.name && (type === 'text')) ? element.attribs.name.value
: null;
if (id || name) {
this.inputsInfo.push({
'id': id,
'name': name,
'location': element.openLineCol
});
} else {
return new Issue('E033', element.openLineCol, {
'idValue': 'null',
'nameValue': 'null'
});
}
return [];
}n/a
isLabeable = function (ele) {
if (ele.type !== 'tag' || !lodash.includes(elems, ele.name)) {
// element isn't a tag or isn't a labeable element
return false;
}
if (ele.name === 'input' && ele.attribs && ele.attribs.type &&
ele.attribs.type.value === 'hidden') {
// inputs that are hidden are not labeable elements
return false;
}
// element passed all the tests
return true;
}...
var id = ele.attribs['for'].value,
forElement = this.idmap[id];
if (!forElement) {
// the paired element does not exist
return new Issue('E021', ele.openLineCol, { id: id });
} else if (!knife.isLabeable(forElement)) {
return new Issue('E022', ele.openLineCol, { id: id });
}
}
return [];
};
...isVoidElement = function (tagName) {
return elems.indexOf(tagName) !== -1;
}...
ele.dupes = this.dupes;
this.dupes = [];
};
DomBuilder.prototype.onclosetag = function () {
var ele = this._tagStack[this._tagStack.length - 1];
if (ele && !knife.isVoidElement(ele.name)) {
// Mercifully, no whitespace is allowed between < and /
this.was_closed = this.htmlText[this.parser.startIndex + 1] === '/';
ele.close = this.was_closed
? this.htmlText.slice(this.parser.startIndex + 2, this.parser.endIndex)
: '';
ele.closeIndex = this.parser.startIndex;
if (!this.was_closed && ele.closeIndex == ele.openIndex) {
...buildIdMap = function (originElement) {
var rElem = originElement;
while (rElem.parent !== null) {
rElem = rElem.parent;
}
while (rElem.prev !== null) {
rElem = rElem.prev;
}
var roots = [];
while (rElem !== null) {
roots.push(rElem);
rElem = rElem.next;
}
var idmap = {};
roots.forEach(function iterateElements(element) {
if (element.attribs && element.attribs.id) {
var id = element.attribs.id.value;
if (!idmap.hasOwnProperty(id)) {
idmap[id] = element;
}
}
if (element.children) {
element.children.forEach(iterateElements);
}
});
this.idmap = idmap;
}...
return new Issue('E019', ele.openLineCol);
} else if (!strict && !hasFor && !this.hasValidChild(ele)) {
return new Issue('E020', ele.openLineCol);
}
if (hasFor) {
if (!this.idmap) {
this.buildIdMap(ele);
}
var id = ele.attribs['for'].value,
forElement = this.idmap[id];
if (!forElement) {
// the paired element does not exist
...end = function () {
this.idmap = null;
}n/a
hasValidChild = function (ele) {
// test for any element to be labeable
return lodash.some(ele.children, knife.isLabeable);
}...
// whether or not all labels MUST have a for attr,
// regardless of whether the label has children
var strict = (opts[this.name] === 'strict'),
hasFor = ele.attribs.hasOwnProperty('for');
if (strict && !hasFor) {
return new Issue('E019', ele.openLineCol);
} else if (!strict && !hasFor && !this.hasValidChild(ele)) {
return new Issue('E020', ele.openLineCol);
}
if (hasFor) {
if (!this.idmap) {
this.buildIdMap(ele);
}
...lint = function (ele, opts) {
if (!opts[this.name]) {
return [];
}
// whether or not all labels MUST have a for attr,
// regardless of whether the label has children
var strict = (opts[this.name] === 'strict'),
hasFor = ele.attribs.hasOwnProperty('for');
if (strict && !hasFor) {
return new Issue('E019', ele.openLineCol);
} else if (!strict && !hasFor && !this.hasValidChild(ele)) {
return new Issue('E020', ele.openLineCol);
}
if (hasFor) {
if (!this.idmap) {
this.buildIdMap(ele);
}
var id = ele.attribs['for'].value,
forElement = this.idmap[id];
if (!forElement) {
// the paired element does not exist
return new Issue('E021', ele.openLineCol, { id: id });
} else if (!knife.isLabeable(forElement)) {
return new Issue('E022', ele.openLineCol, { id: id });
}
}
return [];
}n/a
lint = function (element, opts) {
var a = element.attribs;
if (a && a.hasOwnProperty('lang')) {
var l = a.lang.value;
if (opts['lang-style']) {
var valid = knife.checkLangTag(l);
if (valid === 1) {
return new Issue('E038', a.lang.valueLineCol, {lang:l});
}
if (opts['lang-style'] === 'case' && valid === 2) {
return new Issue('E039', a.lang.valueLineCol, {lang:l});
}
}
return [];
}
return opts['html-req-lang'] ?
new Issue('E025', element.openLineCol) : [];
}n/a
checkLangTag = function (l) {
if (!l || l.length === 0) { return 0; }
var n = l.lastIndexOf('-');
var lang = '', country = '';
if (n === -1) { lang = l; }
else { lang = l.slice(0,n); country = l.slice(n + 1, l.length); }
return (checkLang(lang) && checkCountry(country)) ? 0
: (checkLang(lang.toLowerCase()) && checkCountry(country.toUpperCase())) ? 2
: 1;
}...
};
module.exports.lint = function (element, opts) {
var a = element.attribs;
if (a && a.hasOwnProperty('lang')) {
var l = a.lang.value;
if (opts['lang-style']) {
var valid = knife.checkLangTag(l);
if (valid === 1) {
return new Issue('E038', a.lang.valueLineCol, {lang:l});
}
if (opts['lang-style'] === 'case' && valid === 2) {
return new Issue('E039', a.lang.valueLineCol, {lang:l});
}
}
...lint = function (lines, opts, inlineConfigs) {
lines[0] = '';
var subs = this.subscribers;
// use the opts as our base, and build from them.
inlineConfigs.reset(opts);
return lodash.flattenDeep(lines.map(function (line, index) {
/*
* Right now, if the config is on a line, that whole line is
* given the new configuration. This is not great in theory,
* but in practice line rules don't really need the split.
*/
inlineConfigs.getOptsAtIndex(line.index);
if (index === 0) { return []; }
return knife.applyRules(subs, line, inlineConfigs.current);
}));
}n/a
lint = function (line, opts) {
var format = opts[this.name];
if (!format) {
return [];
}
format = format.toLowerCase();
var regex = {
cr: /(^|[^\n\r])\r$/,
lf: /(^|[^\n\r])\n$/,
crlf: /(^|[^\n\r])\r\n$/
}[format];
if (regex.test(line.line)) {
return [];
}
var len = line.line.length,
pos = [line.row, len];
if (line.line[len - 2] === '\r') {
pos[1] -= 1;
}
return new Issue('E015', pos, { format: format });
}n/a
lint = function (line, opts) {
var maxLength = opts[this.name];
var ignoreRegExpString = opts[this.name + '-ignore-regex'];
var lineText;
var len;
var pos;
if (!maxLength) {
return [];
}
lineText = line.line.replace(/(\r\n|\n|\r)$/, '');
if (ignoreRegExpString && (new RegExp(ignoreRegExpString, 'g')).test(lineText)) {
return [];
}
len = lineText.length;
if (len <= maxLength) {
return [];
}
pos = [line.row, len];
return new Issue('E040', pos, { maxlength: maxLength, length: len });
}n/a
matchFilter = function (data, rule) {
if (!rule.filter) {
return true;
}
return rule.filter.indexOf(data.toLowerCase()) > -1;
}n/a
renderIssue = function (issue) {
return this.renderMsg(issue.code, issue.data);
}n/a
renderMsg = function (code, data) {
var format = errors[code];
return lodash.template(format)(data);
}n/a
lint = function (elt, opts) {
var output = [];
var titles = elt.children.filter(function (e) {
return e.type === 'tag' && e.name === 'title';
});
if (opts['head-req-title'] &&
!titles.some(function(t){return t.children.length > 0;})) {
output.push(new Issue('E027', elt.openLineCol));
}
if (opts['title-no-dup'] && titles.length > 1) {
output.push(new Issue('E028', titles[1].openLineCol,
{ num: titles.length }));
}
var maxlen = opts['title-max-len'];
if (maxlen) { titles.map(function(t) {
var text = t.children.filter(function(c) {return c.type === 'text';})
.map(function(c) { return c.data; }).join('');
if (text.length > maxlen) {
output.push(new Issue('E029', t.openLineCol,
{ title: text, maxlength: maxlen }));
}
}); }
return output;
}n/a
getLineColFunc = function (htmlText, lineCol) {
var lastInd = 0,
line = 0,
col = 0;
if (lineCol && lineCol[0] && lineCol[1]) {
line = lineCol[0];
col = lineCol[1];
}
return function (i) {
if (i < lastInd) {
throw new Error('Index passed to line/column' + ' function (' + i + ') does not keep with order (last was ' + lastInd
+ ')');
}
while (lastInd < i) {
if (htmlText[lastInd] === '\n') {
col = 0;
line++;
} else {
col++;
}
lastInd++;
}
return [line + 1, col + 1];
};
}...
// NOTE: this must be called before parsing begins
DomBuilder.prototype.initialize = function (parser) {
this.parser = parser;
};
DomBuilder.prototype.start = function (htmlText) {
this.htmlText = htmlText;
this.lineColFunc = knife.getLineColFunc(htmlText);
// When a tag has no close, startIndex is too large by 3 for the
// next calls to onopentag and _addDomElement. Keep track of this.
this.was_closed = true;
};
DomBuilder.prototype.onerror = function (error) {
// TODO: actually bubble this up or queue errors
...function RuleList() {
this.rulesMap = {};
this.subsMap = {};
}n/a
fromRuleMap = function (ruleMap) {
var ruleList = new RuleList();
lodash.forOwn(ruleMap, function (rule) {
ruleList.addRule(rule);
});
return ruleList;
}n/a
addRule = function (rule) {
var ruleName = rule.name;
if (this.rulesMap[ruleName]) {
this.removeRule(ruleName);
}
this.subscribeRule(rule);
this.rulesMap[ruleName] = rule;
}...
}
module.exports = RuleList;
RuleList.fromRuleMap = function (ruleMap) {
var ruleList = new RuleList();
lodash.forOwn(ruleMap, function (rule) {
ruleList.addRule(rule);
});
return ruleList;
};
RuleList.prototype.getRule = function (ruleName) {
return this.rulesMap[ruleName];
...forEach = function (func) {
lodash.forOwn(this.rulesMap, function (rule) {
// this function call is wrapped because lodash.forOwn
// passes back 3 args, this method should only pass back 1
func(rule);
});
}...
/**
* Apply the given cofiguration to this.current. Returns true if the operation resulted in any changes, false otherwise.
* @param {Object} config - the new config to write onto the current options.
*/
function applyConfig(config) {
var changed = false;
config.rules.forEach(function (rule) {
// for each rule in the configuration, apply it to this.current
if (rule.type === 'rule') {
if (!(rule.name in this.current)) {
throw new Error('option ' + rule.name + ' does not exist.');
} else {
var cur = this.current[rule.name];
this.current[rule.name] = (rule.value === '$previous')
...getRule = function (ruleName) {
return this.rulesMap[ruleName];
}...
}
this.subscribeRule(rule);
this.rulesMap[ruleName] = rule;
};
RuleList.prototype.removeRule = function (ruleName) {
var rule = this.getRule(ruleName);
if (rule) {
this.unsubscribeRule(rule);
}
delete this.rulesMap[ruleName];
};
...getSubscribers = function (subName) {
var subs = this.subsMap[subName];
return subs ? subs : [];
}n/a
removeRule = function (ruleName) {
var rule = this.getRule(ruleName);
if (rule) {
this.unsubscribeRule(rule);
}
delete this.rulesMap[ruleName];
}...
return subs ? subs : [];
};
RuleList.prototype.addRule = function (rule) {
var ruleName = rule.name;
if (this.rulesMap[ruleName]) {
this.removeRule(ruleName);
}
this.subscribeRule(rule);
this.rulesMap[ruleName] = rule;
};
RuleList.prototype.removeRule = function (ruleName) {
...subscribeRule = function (rule) {
if (!rule.on) {
return;
}
rule.on.forEach(function (subName) {
if (!this.subsMap[subName]) {
this.subsMap[subName] = [];
}
this.subsMap[subName].push(rule);
}.bind(this));
}...
RuleList.prototype.addRule = function (rule) {
var ruleName = rule.name;
if (this.rulesMap[ruleName]) {
this.removeRule(ruleName);
}
this.subscribeRule(rule);
this.rulesMap[ruleName] = rule;
};
RuleList.prototype.removeRule = function (ruleName) {
var rule = this.getRule(ruleName);
if (rule) {
...unsubscribeRule = function (rule) {
if (!rule.on) {
return;
}
rule.on.forEach(function (subName) {
var subIndex = this.subsMap[subName].indexOf(rule);
this.subsMap[subName].splice(subIndex, 1);
}.bind(this));
}...
this.rulesMap[ruleName] = rule;
};
RuleList.prototype.removeRule = function (ruleName) {
var rule = this.getRule(ruleName);
if (rule) {
this.unsubscribeRule(rule);
}
delete this.rulesMap[ruleName];
};
RuleList.prototype.unsubscribeRule = function (rule) {
if (!rule.on) {
...shred = function (html) {
// Take the HTML string
// Return an array of {line, line number, index}
var row = 1,
ind = 0,
shredded = [];
while (html) {
var len = html.search('[\r\n]') + 1;
if (len === 0) { len = html.length; }
else if (html[len - 1] === '\r' && html[len] === '\n') { len++; }
shredded[row] = {
line: html.substr(0, len),
index: ind,
row: row
};
row++; ind += len;
html = html.slice(len);
}
return shredded;
}n/a
lint = function (element, opts) {
// if not enabled, get outta here!
if (!opts[this.name]) {
return [];
}
var issues = [],
lineColFunc = null;
// if it's text - make sure it only has alphanumericals. If it has a &, a ; should follow.
if (['text'].indexOf(element.type) > -1 && element.data.length > 0) {
lineColFunc = knife.getLineColFunc(element.data, element.openLineCol);
[regex.improper, regex.brackets, regex.unescaped].forEach(function (currentRegex) {
var match = executeRegex(currentRegex, element.data);
while (match) {
var lineCol = lineColFunc(match.index);
issues.push(new Issue('E023', lineCol, {
chars: match[1],
part: 'text'
}));
match = executeRegex(currentRegex, element.data);
}
});
}
if (element.attribs) {
var attributeNames = Object.keys(element.attribs);
for (var index = 0; index < attributeNames.length; index++) {
var valueObject = element.attribs[attributeNames[index]];
lineColFunc = knife.getLineColFunc(valueObject.value, valueObject.valueLineCol);
[regex.improper, regex.brackets, regex.unescaped].forEach(function (currentRegex) {
var match = executeRegex(currentRegex, valueObject.value);
while (match) {
var lineCol = lineColFunc(match.index);
issues.push(new Issue('E023', lineCol, {
chars: match[1],
part: 'attribute value'
}));
match = executeRegex(currentRegex, element.data);
}
});
}
}
return issues;
}n/a
lint = function (ele, opts) {
if (!opts[this.name]) {
return [];
}
// get the children of this element
var children = ele.children;
// check for a caption element
for (var i = 0; i < children.length; i++) {
if (children[i].name === 'caption')
{
return [];
}
}
//return an issue
return new Issue('E031', ele.openLineCol);
}n/a
lint = function (ele, opts) {
if (!opts[this.name]) {
return [];
}
var children = ele.children;
var childIndex = 0;
//ffwd to first relevant table child
while (children[childIndex] && children[childIndex].name &&
(children[childIndex].name.match(/caption/i) ||
children[childIndex].name.match(/colgroup/i) ||
children[childIndex].name.match(/tfoot/i)
)) {
childIndex = childIndex + 1;
}
if (children[childIndex] && children[childIndex].name) {
if (children[childIndex].name.match(/thead/i)) {
return [];
}
if (children[childIndex].name.match(/tr/i) && children[childIndex].children[0].name.match(/th/i)) {
return [];
}
}
return new Issue('E035', ele.openLineCol);
}n/a
lint = function (element, opts) {
var matcher = knife.matchFilter.bind(knife, element.name);
var s = this.subscribers.filter(matcher);
return knife.applyRules(s, element, opts);
}n/a
lint = function (element, opts) {
var format = opts[this.name];
if (!format || format.indexOf(element.name) < 0) {
return [];
}
return new Issue('E016', element.openLineCol, {
tag: element.name
});
}n/a
lint = function (element, opts) {
// If the tag did not close itself
if (!element.close ||
element.name.toLowerCase() !== element.close.toLowerCase()) {
if (knife.isVoidElement(element.name)) {
var selfClose = knife.isSelfClosing(element);
var style = opts['tag-self-close'];
if ((style == 'always' && !selfClose) ||
(style == 'never' && selfClose)) {
return new Issue('E018', element.openLineCol,
{expect: style});
}
} else {
if (opts['tag-close']) {
return new Issue('E042', element.openLineCol);
}
}
} else {
if (opts['tag-name-match'] && element.name !== element.close) {
return new Issue('E030', element.closeLineCol);
}
}
return [];
}n/a
lint = function (element, opts) {
if (!opts[this.name] || !uppercaseMask.test(element.name)) {
return [];
}
return new Issue('E017', element.openLineCol);
}n/a
hasNonEmptyAttr = function (tag, attr, allowNull) {
var a = tag.attribs[attr];
return (a && (allowNull || (a.value && a.value.length > 0)));
}...
name: 'img-req-alt',
on: ['tag'],
filter: ['img']
};
module.exports.lint = function (element, opts) {
var opt = opts[this.name];
if (!opt || knife.hasNonEmptyAttr(element, 'alt', opt === 'allownull
')) {
return [];
}
return new Issue('E013', element.openLineCol);
};
...isSelfClosing = function (element) {
var openRaw = element.open;
return openRaw[openRaw.length - 1] === '/';
}...
};
module.exports.lint = function (element, opts) {
// If the tag did not close itself
if (!element.close ||
element.name.toLowerCase() !== element.close.toLowerCase()) {
if (knife.isVoidElement(element.name)) {
var selfClose = knife.isSelfClosing(element);
var style = opts['tag-self-close'];
if ((style == 'always' && !selfClose) ||
(style == 'never' && selfClose)) {
return new Issue('E018', element.openLineCol,
{expect: style});
}
} else {
...