UNPKG

5.29 kBJavaScriptView Raw
1import { parseColor } from './color'
2import { parseBoxShadowValue } from './parseBoxShadowValue'
3import { splitAtTopLevelOnly } from './splitAtTopLevelOnly'
4
5let cssFunctions = ['min', 'max', 'clamp', 'calc']
6
7// Ref: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Types
8
9function isCSSFunction(value) {
10 return cssFunctions.some((fn) => new RegExp(`^${fn}\\(.*\\)`).test(value))
11}
12
13// This is not a data type, but rather a function that can normalize the
14// correct values.
15export function normalize(value, isRoot = true) {
16 // Keep raw strings if it starts with `url(`
17 if (value.includes('url(')) {
18 return value
19 .split(/(url\(.*?\))/g)
20 .filter(Boolean)
21 .map((part) => {
22 if (/^url\(.*?\)$/.test(part)) {
23 return part
24 }
25
26 return normalize(part, false)
27 })
28 .join('')
29 }
30
31 // Convert `_` to ` `, except for escaped underscores `\_`
32 value = value
33 .replace(
34 /([^\\])_+/g,
35 (fullMatch, characterBefore) => characterBefore + ' '.repeat(fullMatch.length - 1)
36 )
37 .replace(/^_/g, ' ')
38 .replace(/\\_/g, '_')
39
40 // Remove leftover whitespace
41 if (isRoot) {
42 value = value.trim()
43 }
44
45 // Add spaces around operators inside math functions like calc() that do not follow an operator
46 // or '('.
47 value = value.replace(/(calc|min|max|clamp)\(.+\)/g, (match) => {
48 return match.replace(
49 /(-?\d*\.?\d(?!\b-.+[,)](?![^+\-/*])\D)(?:%|[a-z]+)?|\))([+\-/*])/g,
50 '$1 $2 '
51 )
52 })
53
54 return value
55}
56
57export function url(value) {
58 return value.startsWith('url(')
59}
60
61export function number(value) {
62 return !isNaN(Number(value)) || isCSSFunction(value)
63}
64
65export function percentage(value) {
66 return (value.endsWith('%') && number(value.slice(0, -1))) || isCSSFunction(value)
67}
68
69let lengthUnits = [
70 'cm',
71 'mm',
72 'Q',
73 'in',
74 'pc',
75 'pt',
76 'px',
77 'em',
78 'ex',
79 'ch',
80 'rem',
81 'lh',
82 'vw',
83 'vh',
84 'vmin',
85 'vmax',
86]
87let lengthUnitsPattern = `(?:${lengthUnits.join('|')})`
88export function length(value) {
89 return (
90 value === '0' ||
91 new RegExp(`^[+-]?[0-9]*\.?[0-9]+(?:[eE][+-]?[0-9]+)?${lengthUnitsPattern}$`).test(value) ||
92 isCSSFunction(value)
93 )
94}
95
96let lineWidths = new Set(['thin', 'medium', 'thick'])
97export function lineWidth(value) {
98 return lineWidths.has(value)
99}
100
101export function shadow(value) {
102 let parsedShadows = parseBoxShadowValue(normalize(value))
103
104 for (let parsedShadow of parsedShadows) {
105 if (!parsedShadow.valid) {
106 return false
107 }
108 }
109
110 return true
111}
112
113export function color(value) {
114 let colors = 0
115
116 let result = splitAtTopLevelOnly(value, '_').every((part) => {
117 part = normalize(part)
118
119 if (part.startsWith('var(')) return true
120 if (parseColor(part, { loose: true }) !== null) return colors++, true
121
122 return false
123 })
124
125 if (!result) return false
126 return colors > 0
127}
128
129export function image(value) {
130 let images = 0
131 let result = splitAtTopLevelOnly(value, ',').every((part) => {
132 part = normalize(part)
133
134 if (part.startsWith('var(')) return true
135 if (
136 url(part) ||
137 gradient(part) ||
138 ['element(', 'image(', 'cross-fade(', 'image-set('].some((fn) => part.startsWith(fn))
139 ) {
140 images++
141 return true
142 }
143
144 return false
145 })
146
147 if (!result) return false
148 return images > 0
149}
150
151let gradientTypes = new Set([
152 'linear-gradient',
153 'radial-gradient',
154 'repeating-linear-gradient',
155 'repeating-radial-gradient',
156 'conic-gradient',
157])
158export function gradient(value) {
159 value = normalize(value)
160
161 for (let type of gradientTypes) {
162 if (value.startsWith(`${type}(`)) {
163 return true
164 }
165 }
166 return false
167}
168
169let validPositions = new Set(['center', 'top', 'right', 'bottom', 'left'])
170export function position(value) {
171 let positions = 0
172 let result = splitAtTopLevelOnly(value, '_').every((part) => {
173 part = normalize(part)
174
175 if (part.startsWith('var(')) return true
176 if (validPositions.has(part) || length(part) || percentage(part)) {
177 positions++
178 return true
179 }
180
181 return false
182 })
183
184 if (!result) return false
185 return positions > 0
186}
187
188export function familyName(value) {
189 let fonts = 0
190 let result = splitAtTopLevelOnly(value, ',').every((part) => {
191 part = normalize(part)
192
193 if (part.startsWith('var(')) return true
194
195 // If it contains spaces, then it should be quoted
196 if (part.includes(' ')) {
197 if (!/(['"])([^"']+)\1/g.test(part)) {
198 return false
199 }
200 }
201
202 // If it starts with a number, it's invalid
203 if (/^\d/g.test(part)) {
204 return false
205 }
206
207 fonts++
208
209 return true
210 })
211
212 if (!result) return false
213 return fonts > 0
214}
215
216let genericNames = new Set([
217 'serif',
218 'sans-serif',
219 'monospace',
220 'cursive',
221 'fantasy',
222 'system-ui',
223 'ui-serif',
224 'ui-sans-serif',
225 'ui-monospace',
226 'ui-rounded',
227 'math',
228 'emoji',
229 'fangsong',
230])
231export function genericName(value) {
232 return genericNames.has(value)
233}
234
235let absoluteSizes = new Set([
236 'xx-small',
237 'x-small',
238 'small',
239 'medium',
240 'large',
241 'x-large',
242 'x-large',
243 'xxx-large',
244])
245export function absoluteSize(value) {
246 return absoluteSizes.has(value)
247}
248
249let relativeSizes = new Set(['larger', 'smaller'])
250export function relativeSize(value) {
251 return relativeSizes.has(value)
252}
253
\No newline at end of file