--- /dev/null
+"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) {
+ return (root, result) => {
+ const validOptions = validateOptions(
+ result,
+ ruleName,
+ {
+ actual: maxLength,
+ possible: _.isNumber
+ },
+ {
+ actual: options,
+ possible: {
+ ignore: ["non-comments", "comments"],
+ ignorePattern: [_.isString]
+ },
+ optional: true
+ }
+ );
+ if (!validOptions) {
+ return;
+ }
+
+ const rootString = root.source.input.css;
+
+ const ignoreNonComments = optionsMatches(options, "ignore", "non-comments");
+ const ignoreComments = optionsMatches(options, "ignore", "comments");
+
+ // Check first line
+ checkNewline({ endIndex: 0 });
+
+ // Check subsequent lines
+ styleSearch(
+ { source: rootString, target: ["\n"], comments: "check" },
+ checkNewline
+ );
+
+ function complain(index) {
+ report({
+ index,
+ result,
+ ruleName,
+ message: messages.expected(maxLength),
+ node: root
+ });
+ }
+
+ function checkNewline(match) {
+ 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, "sub[0].length", 0);
+ },
+ 0
+ );
+
+ const importUrlsLength = execall(
+ /@import\s+(['"].*['"])/gi,
+ lineText
+ ).reduce((result, match) => {
+ return result + _.get(match, "sub[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);
+ }
+
+ // 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);
+ }
+
+ // 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);
+ }
+ };
+};
+
+rule.ruleName = ruleName;
+rule.messages = messages;
+module.exports = rule;