99 lines
3.2 KiB
JavaScript
99 lines
3.2 KiB
JavaScript
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);
|