UNPKG

4.87 kBJavaScriptView Raw
1'use strict';
2
3const valueParser = require('postcss-value-parser');
4
5const declarationValueIndex = require('../../utils/declarationValueIndex');
6const getDeclarationValue = require('../../utils/getDeclarationValue');
7const isStandardSyntaxValue = require('../../utils/isStandardSyntaxValue');
8const optionsMatches = require('../../utils/optionsMatches');
9const report = require('../../utils/report');
10const ruleMessages = require('../../utils/ruleMessages');
11const setDeclarationValue = require('../../utils/setDeclarationValue');
12const validateOptions = require('../../utils/validateOptions');
13const { isRegExp, isString, assert } = require('../../utils/validateTypes');
14
15const ruleName = 'alpha-value-notation';
16
17const messages = ruleMessages(ruleName, {
18 expected: (unfixed, fixed) => `Expected "${unfixed}" to be "${fixed}"`,
19});
20
21const meta = {
22 url: 'https://stylelint.io/user-guide/rules/list/alpha-value-notation',
23};
24
25const ALPHA_PROPS = new Set(['opacity', 'shape-image-threshold']);
26const ALPHA_FUNCS = new Set(['hsl', 'hsla', 'hwb', 'lab', 'lch', 'rgb', 'rgba']);
27
28/** @type {import('stylelint').Rule} */
29const rule = (primary, secondaryOptions, context) => {
30 return (root, result) => {
31 const validOptions = validateOptions(
32 result,
33 ruleName,
34 {
35 actual: primary,
36 possible: ['number', 'percentage'],
37 },
38 {
39 actual: secondaryOptions,
40 possible: {
41 exceptProperties: [isString, isRegExp],
42 },
43 optional: true,
44 },
45 );
46
47 if (!validOptions) return;
48
49 const optionFuncs = Object.freeze({
50 number: {
51 expFunc: isNumber,
52 fixFunc: asNumber,
53 },
54 percentage: {
55 expFunc: isPercentage,
56 fixFunc: asPercentage,
57 },
58 });
59
60 root.walkDecls((decl) => {
61 let needsFix = false;
62 const parsedValue = valueParser(getDeclarationValue(decl));
63
64 parsedValue.walk((node) => {
65 /** @type {import('postcss-value-parser').Node | undefined} */
66 let alpha;
67
68 if (ALPHA_PROPS.has(decl.prop.toLowerCase())) {
69 alpha = findAlphaInValue(node);
70 } else {
71 if (node.type !== 'function') return;
72
73 if (!ALPHA_FUNCS.has(node.value.toLowerCase())) return;
74
75 alpha = findAlphaInFunction(node);
76 }
77
78 if (!alpha) return;
79
80 const { value } = alpha;
81
82 if (!isStandardSyntaxValue(value)) return;
83
84 if (!isNumber(value) && !isPercentage(value)) return;
85
86 /** @type {'number' | 'percentage'} */
87 let expectation = primary;
88
89 if (optionsMatches(secondaryOptions, 'exceptProperties', decl.prop)) {
90 if (expectation === 'number') {
91 expectation = 'percentage';
92 } else if (expectation === 'percentage') {
93 expectation = 'number';
94 }
95 }
96
97 if (optionFuncs[expectation].expFunc(value)) return;
98
99 const fixed = optionFuncs[expectation].fixFunc(value);
100 const unfixed = value;
101
102 if (context.fix) {
103 alpha.value = String(fixed);
104 needsFix = true;
105
106 return;
107 }
108
109 report({
110 message: messages.expected(unfixed, fixed),
111 node: decl,
112 index: declarationValueIndex(decl) + alpha.sourceIndex,
113 result,
114 ruleName,
115 });
116 });
117
118 if (needsFix) {
119 setDeclarationValue(decl, parsedValue.toString());
120 }
121 });
122 };
123};
124
125/**
126 * @param {string} value
127 * @returns {string}
128 */
129function asPercentage(value) {
130 const number = Number(value);
131
132 return `${Number((number * 100).toPrecision(3))}%`;
133}
134
135/**
136 * @param {string} value
137 * @returns {string}
138 */
139function asNumber(value) {
140 const dimension = valueParser.unit(value);
141
142 assert(dimension);
143
144 const number = Number(dimension.number);
145
146 return Number((number / 100).toPrecision(3)).toString();
147}
148
149/**
150 * @template {import('postcss-value-parser').Node} T
151 * @param {T} node
152 * @returns {T | undefined}
153 */
154function findAlphaInValue(node) {
155 return node.type === 'word' || node.type === 'function' ? node : undefined;
156}
157
158/**
159 * @param {import('postcss-value-parser').FunctionNode} node
160 * @returns {import('postcss-value-parser').Node | undefined}
161 */
162function findAlphaInFunction(node) {
163 const args = node.nodes.filter(({ type }) => type === 'word' || type === 'function');
164
165 if (args.length === 4) return args[3];
166
167 const slashNodeIndex = node.nodes.findIndex(({ type, value }) => type === 'div' && value === '/');
168
169 if (slashNodeIndex !== -1) {
170 const nodesAfterSlash = node.nodes.slice(slashNodeIndex + 1, node.nodes.length);
171
172 return nodesAfterSlash.find(({ type }) => type === 'word');
173 }
174
175 return undefined;
176}
177
178/**
179 * @param {string} value
180 * @returns {boolean}
181 */
182function isPercentage(value) {
183 const dimension = valueParser.unit(value);
184
185 return dimension && dimension.unit === '%';
186}
187
188/**
189 * @param {string} value
190 * @returns {boolean}
191 */
192function isNumber(value) {
193 const dimension = valueParser.unit(value);
194
195 return dimension && dimension.unit === '';
196}
197
198rule.ruleName = ruleName;
199rule.messages = messages;
200rule.meta = meta;
201module.exports = rule;