1 | import { parseColor } from './color'
|
2 | import { parseBoxShadowValue } from './parseBoxShadowValue'
|
3 | import { splitAtTopLevelOnly } from './splitAtTopLevelOnly'
|
4 |
|
5 | let cssFunctions = ['min', 'max', 'clamp', 'calc']
|
6 |
|
7 | // Ref: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Types
|
8 |
|
9 | function 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.
|
15 | export 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 |
|
57 | export function url(value) {
|
58 | return value.startsWith('url(')
|
59 | }
|
60 |
|
61 | export function number(value) {
|
62 | return !isNaN(Number(value)) || isCSSFunction(value)
|
63 | }
|
64 |
|
65 | export function percentage(value) {
|
66 | return (value.endsWith('%') && number(value.slice(0, -1))) || isCSSFunction(value)
|
67 | }
|
68 |
|
69 | let 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 | ]
|
87 | let lengthUnitsPattern = `(?:${lengthUnits.join('|')})`
|
88 | export 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 |
|
96 | let lineWidths = new Set(['thin', 'medium', 'thick'])
|
97 | export function lineWidth(value) {
|
98 | return lineWidths.has(value)
|
99 | }
|
100 |
|
101 | export 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 |
|
113 | export 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 |
|
129 | export 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 |
|
151 | let gradientTypes = new Set([
|
152 | 'linear-gradient',
|
153 | 'radial-gradient',
|
154 | 'repeating-linear-gradient',
|
155 | 'repeating-radial-gradient',
|
156 | 'conic-gradient',
|
157 | ])
|
158 | export 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 |
|
169 | let validPositions = new Set(['center', 'top', 'right', 'bottom', 'left'])
|
170 | export 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 |
|
188 | export 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 |
|
216 | let 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 | ])
|
231 | export function genericName(value) {
|
232 | return genericNames.has(value)
|
233 | }
|
234 |
|
235 | let 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 | ])
|
245 | export function absoluteSize(value) {
|
246 | return absoluteSizes.has(value)
|
247 | }
|
248 |
|
249 | let relativeSizes = new Set(['larger', 'smaller'])
|
250 | export function relativeSize(value) {
|
251 | return relativeSizes.has(value)
|
252 | }
|
253 |
|
\ | No newline at end of file |