fix
This commit is contained in:
98
tools/stylelint-plugin-pi-theme/no-raw-color-token.mjs
Normal file
98
tools/stylelint-plugin-pi-theme/no-raw-color-token.mjs
Normal file
@@ -0,0 +1,98 @@
|
||||
import stylelint from 'stylelint';
|
||||
|
||||
const { createPlugin, utils } = stylelint;
|
||||
const { report, ruleMessages, validateOptions } = utils;
|
||||
|
||||
const ruleName = 'pi/no-raw-color-token';
|
||||
|
||||
const messages = ruleMessages(ruleName, {
|
||||
rejected: (token) =>
|
||||
`"${token}" se usa directamente. Los componentes de la librería deben ` +
|
||||
`consumir un token --pi-theme-* con este color como fallback ` +
|
||||
`(var(--pi-theme-<algo>, ${token})) para que un tema o el modo oscuro ` +
|
||||
`puedan redefinir la superficie sin tocar el componente. Ver TOKENS.md.`,
|
||||
});
|
||||
|
||||
const meta = {
|
||||
url: 'https://github.com/pi/ui/tools/stylelint-plugin-pi-theme',
|
||||
};
|
||||
|
||||
/**
|
||||
* Contrato de esta librería (ver TOKENS.md): un componente NO consume
|
||||
* `--pi-color-*` en crudo, lo hace a través de un `--pi-theme-*` con el color
|
||||
* como fallback: `var(--pi-theme-x, var(--pi-color-y))`. Esta regla detecta
|
||||
* el caso contrario — `--pi-color-*` referenciado fuera de cualquier
|
||||
* fallback de `--pi-theme-*` — que es exactamente la clase de bug que dejó
|
||||
* colores de marca fijos filtrando en modo oscuro (accordion/tab/select en
|
||||
* esta misma librería, antes de tokenizarlos).
|
||||
*
|
||||
* Deliberadamente NO seguimos con `postcss-value-parser`: los valores aquí
|
||||
* son casi siempre `var(...)` con como mucho una función envolvente
|
||||
* (`linear-gradient(...)`, `rgba(...)`), así que un escaneo con paréntesis
|
||||
* balanceados es suficiente y evita añadir una dependencia más solo para
|
||||
* esta regla.
|
||||
*/
|
||||
function findMatchingParenEnd(value, openParenIndex) {
|
||||
let depth = 1;
|
||||
for (let i = openParenIndex + 1; i < value.length; i++) {
|
||||
if (value[i] === '(') depth++;
|
||||
else if (value[i] === ')') {
|
||||
depth--;
|
||||
if (depth === 0) return i;
|
||||
}
|
||||
}
|
||||
return value.length - 1;
|
||||
}
|
||||
|
||||
/** Rangos `[start, end)` de cada fallback de un `var(--pi-theme-*, ...)` — lo que haya dentro está exento. */
|
||||
function findProtectedRanges(value) {
|
||||
const ranges = [];
|
||||
const pattern = /var\(\s*--pi-theme-[\w-]+/g;
|
||||
let match;
|
||||
while ((match = pattern.exec(value))) {
|
||||
const openParenIndex = match.index + match[0].indexOf('(');
|
||||
const end = findMatchingParenEnd(value, openParenIndex);
|
||||
ranges.push([match.index, end]);
|
||||
}
|
||||
return ranges;
|
||||
}
|
||||
|
||||
function isProtected(index, ranges) {
|
||||
return ranges.some(([start, end]) => index > start && index < end);
|
||||
}
|
||||
|
||||
const rule = (primaryOption) => {
|
||||
return (root, result) => {
|
||||
const validOptions = validateOptions(result, ruleName, {
|
||||
actual: primaryOption,
|
||||
possible: [true, false],
|
||||
});
|
||||
if (!validOptions || !primaryOption) return;
|
||||
|
||||
root.walkDecls((decl) => {
|
||||
const value = decl.value;
|
||||
if (!value || !value.includes('--pi-color-')) return;
|
||||
|
||||
const protectedRanges = findProtectedRanges(value);
|
||||
const colorPattern = /--pi-color-[\w-]+/g;
|
||||
let match;
|
||||
while ((match = colorPattern.exec(value))) {
|
||||
if (isProtected(match.index, protectedRanges)) continue;
|
||||
|
||||
report({
|
||||
message: messages.rejected(match[0]),
|
||||
node: decl,
|
||||
word: match[0],
|
||||
result,
|
||||
ruleName,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
rule.ruleName = ruleName;
|
||||
rule.messages = messages;
|
||||
rule.meta = meta;
|
||||
|
||||
export default createPlugin(ruleName, rule);
|
||||
Reference in New Issue
Block a user