mirror of
https://github.com/OliveTin/OliveTin
synced 2025-12-16 19:15:38 +00:00
484 lines
12 KiB
JavaScript
484 lines
12 KiB
JavaScript
import { RULE_NAME_ALL } from './constants.mjs';
|
|
|
|
import {
|
|
DISABLE_COMMAND,
|
|
DISABLE_LINE_COMMAND,
|
|
DISABLE_NEXT_LINE_COMMAND,
|
|
ENABLE_COMMAND,
|
|
extractConfigurationComment,
|
|
getConfigurationComment,
|
|
isConfigurationComment,
|
|
} from './utils/configurationComment.mjs';
|
|
import { assert, assertNumber, assertString } from './utils/validateTypes.mjs';
|
|
import { isAtRule, isComment, isDeclaration, isRule } from './utils/typeGuards.mjs';
|
|
import isStandardSyntaxComment from './utils/isStandardSyntaxComment.mjs';
|
|
|
|
import { isTokenComment, tokenize } from '@csstools/css-tokenizer';
|
|
|
|
/** @import {Node as PostcssNode, Comment as PostcssComment, Document as PostcssDocument, Root as PostcssRoot, Source as PostcssSource} from 'postcss' */
|
|
/** @import {DisabledRange, DisabledRangeObject, PostcssResult} from 'stylelint' */
|
|
/** @typedef {Pick<PostcssSource, 'start' | 'end'>} Source */
|
|
|
|
/**
|
|
* @param {PostcssNode} node
|
|
* @param {number} start
|
|
* @param {boolean} strictStart
|
|
* @param {string|undefined} description
|
|
* @param {number} [end]
|
|
* @param {boolean} [strictEnd]
|
|
* @returns {DisabledRange}
|
|
*/
|
|
function createDisableRange(node, start, strictStart, description, end, strictEnd) {
|
|
return {
|
|
node,
|
|
start,
|
|
end: end || undefined,
|
|
strictStart,
|
|
strictEnd: typeof strictEnd === 'boolean' ? strictEnd : undefined,
|
|
description,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Run it like a PostCSS plugin
|
|
* @param {PostcssRoot | PostcssDocument} root
|
|
* @param {PostcssResult} result
|
|
* @returns {PostcssResult}
|
|
*/
|
|
export default function assignDisabledRanges(root, result) {
|
|
/**
|
|
* Most of the functions below work via side effects mutating this object
|
|
* @type {DisabledRangeObject}
|
|
*/
|
|
const disabledRanges = result.stylelint.disabledRanges;
|
|
|
|
/** @type {DisabledRange[]} */
|
|
const disabledRangesAll = [];
|
|
|
|
disabledRanges[RULE_NAME_ALL] = disabledRangesAll;
|
|
|
|
// Work around postcss/postcss-scss#109 by merging adjacent `//` comments
|
|
// into a single node before passing to `checkComment`.
|
|
|
|
/** @type {PostcssComment?} */
|
|
let inlineEnd;
|
|
|
|
const configurationComment = result.stylelint.config?.configurationComment;
|
|
|
|
root.walk((node) => {
|
|
if (isComment(node)) {
|
|
if (inlineEnd) {
|
|
// Ignore comments already processed by grouping with a previous one.
|
|
if (inlineEnd === node) inlineEnd = null;
|
|
|
|
return;
|
|
}
|
|
|
|
const nextComment = node.next();
|
|
|
|
// If any of these conditions are not met, do not merge comments.
|
|
if (
|
|
!(
|
|
!isStandardSyntaxComment(node) &&
|
|
isConfigurationComment(node.text, configurationComment) &&
|
|
nextComment &&
|
|
isComment(nextComment) &&
|
|
(node.text.includes('--') || nextComment.text.startsWith('--'))
|
|
)
|
|
) {
|
|
checkComment(node, node.source, node.text);
|
|
|
|
return;
|
|
}
|
|
|
|
let lastLine = node.source?.end?.line ?? 0;
|
|
const fullComment = node.clone();
|
|
|
|
let current = nextComment;
|
|
|
|
while (
|
|
!isStandardSyntaxComment(current) &&
|
|
!isConfigurationComment(current.text, configurationComment)
|
|
) {
|
|
const currentLine = current.source?.end?.line ?? 0;
|
|
|
|
if (lastLine + 1 !== currentLine) break;
|
|
|
|
fullComment.text += `\n${current.text}`;
|
|
|
|
if (fullComment.source && current.source) {
|
|
fullComment.source.end = current.source.end;
|
|
}
|
|
|
|
inlineEnd = current;
|
|
const next = current.next();
|
|
|
|
if (!next || !isComment(next)) break;
|
|
|
|
current = next;
|
|
lastLine = currentLine;
|
|
}
|
|
|
|
checkComment(fullComment, fullComment.source, fullComment.text);
|
|
}
|
|
|
|
if (isRule(node)) {
|
|
let offset = 0;
|
|
const selector = node.raws?.selector?.raw;
|
|
|
|
checkCommentsInNode(node, selector, offset);
|
|
|
|
offset += selector?.length ?? node.selector.length;
|
|
const between = node.raws?.between;
|
|
|
|
checkCommentsInNode(node, between, offset);
|
|
}
|
|
|
|
if (isAtRule(node)) {
|
|
let offset = node.name.length + 1; // `@` + name
|
|
const afterName = node.raws?.afterName;
|
|
|
|
checkCommentsInNode(node, afterName, offset);
|
|
|
|
offset += afterName?.length ?? 0;
|
|
const params = node.raws?.params?.raw;
|
|
|
|
checkCommentsInNode(node, params, offset);
|
|
|
|
offset += params?.length ?? node.params.length;
|
|
const between = node.raws?.between;
|
|
|
|
checkCommentsInNode(node, between, offset);
|
|
}
|
|
|
|
if (isDeclaration(node)) {
|
|
let offset = node.prop.length;
|
|
const between = node.raws?.between;
|
|
|
|
checkCommentsInNode(node, between, offset);
|
|
|
|
offset += between?.length ?? 0;
|
|
const value = node.raws?.value?.raw;
|
|
|
|
checkCommentsInNode(node, value, offset);
|
|
}
|
|
});
|
|
|
|
return result;
|
|
|
|
/**
|
|
* @param {PostcssNode} node
|
|
* @param {Source} source
|
|
* @param {string} text
|
|
*/
|
|
function processDisableLineCommand(node, source, text) {
|
|
if (source.start) {
|
|
const line = source.start.line;
|
|
const description = getDescription(text);
|
|
|
|
for (const ruleName of getCommandRules(DISABLE_LINE_COMMAND, text)) {
|
|
disableLine(node, line, ruleName, description);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {PostcssNode} node
|
|
* @param {Source} source
|
|
* @param {string} text
|
|
*/
|
|
function processDisableNextLineCommand(node, source, text) {
|
|
if (source.end) {
|
|
const line = source.end.line;
|
|
const description = getDescription(text);
|
|
|
|
for (const ruleName of getCommandRules(DISABLE_NEXT_LINE_COMMAND, text)) {
|
|
disableLine(node, line + 1, ruleName, description);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {PostcssNode} node
|
|
* @param {number} line
|
|
* @param {string} ruleName
|
|
* @param {string|undefined} description
|
|
*/
|
|
function disableLine(node, line, ruleName, description) {
|
|
if (ruleIsDisabled(RULE_NAME_ALL)) {
|
|
throw node.error('All rules have already been disabled', {
|
|
plugin: 'stylelint',
|
|
});
|
|
}
|
|
|
|
if (ruleName === RULE_NAME_ALL) {
|
|
for (const disabledRuleName of Object.keys(disabledRanges)) {
|
|
if (ruleIsDisabled(disabledRuleName)) continue;
|
|
|
|
const strict = disabledRuleName === RULE_NAME_ALL;
|
|
|
|
startDisabledRange(node, line, disabledRuleName, strict, description);
|
|
endDisabledRange(line, disabledRuleName, strict);
|
|
}
|
|
} else {
|
|
if (ruleIsDisabled(ruleName)) {
|
|
throw node.error(`"${ruleName}" has already been disabled`, {
|
|
plugin: 'stylelint',
|
|
});
|
|
}
|
|
|
|
startDisabledRange(node, line, ruleName, true, description);
|
|
endDisabledRange(line, ruleName, true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {PostcssNode} node
|
|
* @param {Source} source
|
|
* @param {string} text
|
|
*/
|
|
function processDisableCommand(node, source, text) {
|
|
const description = getDescription(text);
|
|
|
|
for (const ruleToDisable of getCommandRules(DISABLE_COMMAND, text)) {
|
|
const isAllRules = ruleToDisable === RULE_NAME_ALL;
|
|
|
|
if (ruleIsDisabled(ruleToDisable)) {
|
|
throw node.error(
|
|
isAllRules
|
|
? 'All rules have already been disabled'
|
|
: `"${ruleToDisable}" has already been disabled`,
|
|
{
|
|
plugin: 'stylelint',
|
|
},
|
|
);
|
|
}
|
|
|
|
if (source.start) {
|
|
const line = source.start.line;
|
|
|
|
if (isAllRules) {
|
|
for (const ruleName of Object.keys(disabledRanges)) {
|
|
startDisabledRange(node, line, ruleName, ruleName === RULE_NAME_ALL, description);
|
|
}
|
|
} else {
|
|
startDisabledRange(node, line, ruleToDisable, true, description);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {PostcssNode} node
|
|
* @param {Source} source
|
|
* @param {string} text
|
|
*/
|
|
function processEnableCommand(node, source, text) {
|
|
for (const ruleToEnable of getCommandRules(ENABLE_COMMAND, text)) {
|
|
// need fallback if endLine will be undefined
|
|
const endLine = source.end?.line;
|
|
|
|
assertNumber(endLine);
|
|
|
|
if (ruleToEnable === RULE_NAME_ALL) {
|
|
if (
|
|
Object.values(disabledRanges).every((ranges) => {
|
|
if (ranges.length === 0) return true;
|
|
|
|
const lastRange = ranges[ranges.length - 1];
|
|
|
|
return lastRange && typeof lastRange.end === 'number';
|
|
})
|
|
) {
|
|
throw node.error('No rules have been disabled', {
|
|
plugin: 'stylelint',
|
|
});
|
|
}
|
|
|
|
for (const [ruleName, ranges] of Object.entries(disabledRanges)) {
|
|
const lastRange = ranges[ranges.length - 1];
|
|
|
|
if (!lastRange || !lastRange.end) {
|
|
endDisabledRange(endLine, ruleName, ruleName === RULE_NAME_ALL);
|
|
}
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if (ruleIsDisabled(RULE_NAME_ALL) && disabledRanges[ruleToEnable] === undefined) {
|
|
// Get a starting point from the where all rules were disabled
|
|
disabledRanges[ruleToEnable] = disabledRangesAll.map(({ start, end, description }) =>
|
|
createDisableRange(node, start, false, description, end, false),
|
|
);
|
|
|
|
endDisabledRange(endLine, ruleToEnable, true);
|
|
|
|
continue;
|
|
}
|
|
|
|
if (ruleIsDisabled(ruleToEnable)) {
|
|
endDisabledRange(endLine, ruleToEnable, true);
|
|
|
|
continue;
|
|
}
|
|
|
|
throw node.error(`"${ruleToEnable}" has not been disabled`, {
|
|
plugin: 'stylelint',
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {PostcssNode} node
|
|
* @param {string | undefined} part
|
|
* @param {number} offset
|
|
*/
|
|
function checkCommentsInNode(node, part, offset) {
|
|
if (!(part && part.includes('/*') && part.includes('*/'))) return;
|
|
|
|
tokenize({ css: part }).forEach((token) => {
|
|
if (!isTokenComment(token)) return;
|
|
|
|
const [, text, start, end] = token;
|
|
|
|
const source = node.rangeBy({
|
|
index: start + offset,
|
|
endIndex: end + 1 + offset,
|
|
});
|
|
|
|
checkComment(node, source, text.slice(2, -2).trim());
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @param {PostcssNode} node
|
|
* @param {Source | undefined} source
|
|
* @param {string} text
|
|
*/
|
|
function checkComment(node, source, text) {
|
|
if (!source) return;
|
|
|
|
// Ignore comments that are not relevant commands
|
|
if (!isConfigurationComment(text, configurationComment)) {
|
|
return;
|
|
}
|
|
|
|
switch (extractConfigurationComment(text, configurationComment)) {
|
|
case DISABLE_LINE_COMMAND:
|
|
processDisableLineCommand(node, source, text);
|
|
break;
|
|
case DISABLE_NEXT_LINE_COMMAND:
|
|
processDisableNextLineCommand(node, source, text);
|
|
break;
|
|
case DISABLE_COMMAND:
|
|
processDisableCommand(node, source, text);
|
|
break;
|
|
case ENABLE_COMMAND:
|
|
processEnableCommand(node, source, text);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {string} command
|
|
* @param {string} fullText
|
|
* @returns {string[]}
|
|
*/
|
|
function getCommandRules(command, fullText) {
|
|
// Allow for description (f.e. /* stylelint-disable a, b -- Description */).
|
|
const fullCommand = getConfigurationComment(command, configurationComment);
|
|
const rulesText = fullText.slice(fullCommand.length).split(/\s-{2,}\s/u)[0];
|
|
|
|
assertString(rulesText);
|
|
const rules = rulesText
|
|
.trim()
|
|
.split(',')
|
|
.filter(Boolean)
|
|
.map((r) => r.trim());
|
|
|
|
if (rules.length === 0) {
|
|
return [RULE_NAME_ALL];
|
|
}
|
|
|
|
return rules;
|
|
}
|
|
|
|
/**
|
|
* @param {string} fullText
|
|
* @returns {string|undefined}
|
|
*/
|
|
function getDescription(fullText) {
|
|
const descriptionStart = fullText.indexOf('--');
|
|
|
|
if (descriptionStart === -1) return;
|
|
|
|
return fullText.slice(descriptionStart + 2).trim();
|
|
}
|
|
|
|
/**
|
|
* @param {PostcssNode} node
|
|
* @param {number} line
|
|
* @param {string} ruleName
|
|
* @param {boolean} strict
|
|
* @param {string|undefined} description
|
|
*/
|
|
function startDisabledRange(node, line, ruleName, strict, description) {
|
|
const rangeObj = createDisableRange(node, line, strict, description);
|
|
|
|
ensureRuleRanges(ruleName);
|
|
|
|
const range = disabledRanges[ruleName];
|
|
|
|
assert(range);
|
|
range.push(rangeObj);
|
|
}
|
|
|
|
/**
|
|
* @param {number} line
|
|
* @param {string} ruleName
|
|
* @param {boolean} strict
|
|
*/
|
|
function endDisabledRange(line, ruleName, strict) {
|
|
const ranges = disabledRanges[ruleName];
|
|
const lastRangeForRule = ranges ? ranges[ranges.length - 1] : null;
|
|
|
|
if (!lastRangeForRule) {
|
|
return;
|
|
}
|
|
|
|
// Add an `end` prop to the last range of that rule
|
|
lastRangeForRule.end = line;
|
|
lastRangeForRule.strictEnd = strict;
|
|
}
|
|
|
|
/**
|
|
* @param {string} ruleName
|
|
*/
|
|
function ensureRuleRanges(ruleName) {
|
|
if (!disabledRanges[ruleName]) {
|
|
disabledRanges[ruleName] = disabledRangesAll.map(({ node, start, end, description }) =>
|
|
createDisableRange(node, start, false, description, end, false),
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {string} ruleName
|
|
* @returns {boolean}
|
|
*/
|
|
function ruleIsDisabled(ruleName) {
|
|
const ranges = disabledRanges[ruleName];
|
|
|
|
if (!ranges) return false;
|
|
|
|
const lastRange = ranges[ranges.length - 1];
|
|
|
|
if (!lastRange) return false;
|
|
|
|
if (!lastRange.end) return true;
|
|
|
|
return false;
|
|
}
|
|
}
|