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-, ${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);