123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202 |
- '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',
- };
- /** @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;
|