blob: 366a2597802c32eb2ec78ef5a78a941ab3055761 [file] [log] [blame]
"use strict";
const _ = require("lodash");
const execall = require("execall");
const optionsMatches = require("../../utils/optionsMatches");
const report = require("../../utils/report");
const ruleMessages = require("../../utils/ruleMessages");
const styleSearch = require("style-search");
const validateOptions = require("../../utils/validateOptions");
const ruleName = "max-line-length";
const messages = ruleMessages(ruleName, {
expected: max =>
`Expected line length to be no more than ${max} ${
max === 1 ? "character" : "characters"
}`
});
const rule = function(maxLength, options, context) {
return (root, result) => {
const validOptions = validateOptions(
result,
ruleName,
{
actual: maxLength,
possible: _.isNumber
},
{
actual: options,
possible: {
ignore: ["non-comments", "comments"],
ignorePattern: [_.isString, _.isRegExp]
},
optional: true
}
);
if (!validOptions) {
return;
}
const ignoreNonComments = optionsMatches(options, "ignore", "non-comments");
const ignoreComments = optionsMatches(options, "ignore", "comments");
const rootString = context.fix ? root.toString() : root.source.input.css;
// Check first line
checkNewline(rootString, { endIndex: 0 }, root);
// Check subsequent lines
styleSearch(
{ source: rootString, target: ["\n"], comments: "check" },
match => checkNewline(rootString, match, root)
);
function complain(index, root) {
report({
index,
result,
ruleName,
message: messages.expected(maxLength),
node: root
});
}
function checkNewline(rootString, match, root) {
let nextNewlineIndex = rootString.indexOf("\n", match.endIndex);
if (rootString[nextNewlineIndex - 1] === "\r") {
nextNewlineIndex -= 1;
}
// Accommodate last line
if (nextNewlineIndex === -1) {
nextNewlineIndex = rootString.length;
}
const rawLineLength = nextNewlineIndex - match.endIndex;
const lineText = rootString.slice(match.endIndex, nextNewlineIndex);
// Case sensitive ignorePattern match
if (optionsMatches(options, "ignorePattern", lineText)) {
return;
}
const urlArgumentsLength = execall(/url\((.*)\)/gi, lineText).reduce(
(result, match) => {
return result + _.get(match, "subMatches[0].length", 0);
},
0
);
const importUrlsLength = execall(
/@import\s+(['"].*['"])/gi,
lineText
).reduce((result, match) => {
return result + _.get(match, "subMatches[0].length", 0);
}, 0);
// If the line's length is less than or equal to the specified
// max, ignore it ... So anything below is liable to be complained about.
// **Note that the length of any url arguments or import urls
// are excluded from the calculation.**
if (rawLineLength - urlArgumentsLength - importUrlsLength <= maxLength) {
return;
}
const complaintIndex = nextNewlineIndex - 1;
if (ignoreComments) {
if (match.insideComment) {
return;
}
// This trimming business is to notice when the line starts a
// comment but that comment is indented, e.g.
// /* something here */
const nextTwoChars = rootString
.slice(match.endIndex)
.trim()
.slice(0, 2);
if (nextTwoChars === "/*" || nextTwoChars === "//") {
return;
}
}
if (ignoreNonComments) {
if (match.insideComment) {
return complain(complaintIndex, root);
}
// This trimming business is to notice when the line starts a
// comment but that comment is indented, e.g.
// /* something here */
const nextTwoChars = rootString
.slice(match.endIndex)
.trim()
.slice(0, 2);
if (nextTwoChars !== "/*" && nextTwoChars !== "//") {
return;
}
return complain(complaintIndex, root);
}
// If there are no spaces besides initial (indent) spaces, ignore it
const lineString = rootString.slice(match.endIndex, nextNewlineIndex);
if (lineString.replace(/^\s+/, "").indexOf(" ") === -1) {
return;
}
return complain(complaintIndex, root);
}
};
};
rule.ruleName = ruleName;
rule.messages = messages;
module.exports = rule;