204 lines
5.5 KiB
JavaScript
204 lines
5.5 KiB
JavaScript
'use strict';
|
|
|
|
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 { isNumber, isRegExp, isString, assert } = require('../../utils/validateTypes');
|
|
|
|
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 meta = {
|
|
url: 'https://stylelint.io/user-guide/rules/max-line-length',
|
|
deprecated: true,
|
|
};
|
|
|
|
/** @type {import('stylelint').Rule} */
|
|
const rule = (primary, secondaryOptions, context) => {
|
|
return (root, result) => {
|
|
const validOptions = validateOptions(
|
|
result,
|
|
ruleName,
|
|
{
|
|
actual: primary,
|
|
possible: isNumber,
|
|
},
|
|
{
|
|
actual: secondaryOptions,
|
|
possible: {
|
|
ignore: ['non-comments', 'comments'],
|
|
ignorePattern: [isString, isRegExp],
|
|
},
|
|
optional: true,
|
|
},
|
|
);
|
|
|
|
if (!validOptions) {
|
|
return;
|
|
}
|
|
|
|
if (root.source == null) {
|
|
throw new Error('The root node must have a source');
|
|
}
|
|
|
|
const EXCLUDED_PATTERNS = [
|
|
/url\(\s*(\S.*\S)\s*\)/gi, // allow tab, whitespace in url content
|
|
/@import\s+(['"].*['"])/gi,
|
|
];
|
|
|
|
const ignoreNonComments = optionsMatches(secondaryOptions, 'ignore', 'non-comments');
|
|
const ignoreComments = optionsMatches(secondaryOptions, 'ignore', 'comments');
|
|
const rootString = context.fix ? root.toString() : root.source.input.css;
|
|
// Array of skipped sub strings, i.e `url(...)`, `@import "..."`
|
|
/** @type {Array<[number, number]>} */
|
|
let skippedSubStrings = [];
|
|
let skippedSubStringsIndex = 0;
|
|
|
|
for (const pattern of EXCLUDED_PATTERNS) {
|
|
for (const match of rootString.matchAll(pattern)) {
|
|
const subMatch = match[1] || '';
|
|
const startOfSubString = (match.index || 0) + (match[0] || '').indexOf(subMatch);
|
|
|
|
skippedSubStrings.push([startOfSubString, startOfSubString + subMatch.length]);
|
|
}
|
|
}
|
|
|
|
skippedSubStrings = skippedSubStrings.sort((a, b) => a[0] - b[0]);
|
|
|
|
// Check first line
|
|
checkNewline({ endIndex: 0 });
|
|
// Check subsequent lines
|
|
styleSearch({ source: rootString, target: ['\n'], comments: 'check' }, (match) =>
|
|
checkNewline(match),
|
|
);
|
|
|
|
/**
|
|
* @param {number} index
|
|
*/
|
|
function complain(index) {
|
|
report({
|
|
index,
|
|
result,
|
|
ruleName,
|
|
message: messages.expected(primary),
|
|
node: root,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @param {number} start
|
|
* @param {number} end
|
|
*/
|
|
function tryToPopSubString(start, end) {
|
|
const skippedSubString = skippedSubStrings[skippedSubStringsIndex];
|
|
|
|
assert(skippedSubString);
|
|
const [startSubString, endSubString] = skippedSubString;
|
|
|
|
// Excluded substring does not presented in current line
|
|
if (end < startSubString) {
|
|
return 0;
|
|
}
|
|
|
|
// Compute excluded substring size regarding to current line indexes
|
|
const excluded = Math.min(end, endSubString) - Math.max(start, startSubString);
|
|
|
|
// Current substring is out of range for next lines
|
|
if (endSubString <= end) {
|
|
skippedSubStringsIndex++;
|
|
}
|
|
|
|
return excluded;
|
|
}
|
|
|
|
/**
|
|
* @param {import('style-search').StyleSearchMatch | { endIndex: number }} match
|
|
*/
|
|
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 excludedLength = skippedSubStrings[skippedSubStringsIndex]
|
|
? tryToPopSubString(match.endIndex, nextNewlineIndex)
|
|
: 0;
|
|
const lineText = rootString.slice(match.endIndex, nextNewlineIndex);
|
|
|
|
// Case sensitive ignorePattern match
|
|
if (optionsMatches(secondaryOptions, 'ignorePattern', lineText)) {
|
|
return;
|
|
}
|
|
|
|
// 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 - excludedLength <= primary) {
|
|
return;
|
|
}
|
|
|
|
const complaintIndex = nextNewlineIndex - 1;
|
|
|
|
if (ignoreComments) {
|
|
if ('insideComment' in match && 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 ('insideComment' in match && 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+/, '').includes(' ')) {
|
|
return;
|
|
}
|
|
|
|
return complain(complaintIndex);
|
|
}
|
|
};
|
|
};
|
|
|
|
rule.ruleName = ruleName;
|
|
rule.messages = messages;
|
|
rule.meta = meta;
|
|
module.exports = rule;
|