tailwindcss-patch 9.2.0 → 9.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,326 @@
1
+ export interface BareArbitraryValueOptions {
2
+ /**
3
+ * 允许作为无方括号任意值的单位列表。
4
+ */
5
+ units?: string[]
6
+ }
7
+
8
+ export interface BareArbitraryValueResolveResult {
9
+ candidate: string
10
+ canonicalCandidate: string
11
+ }
12
+
13
+ const DEFAULT_BARE_ARBITRARY_VALUE_UNITS = [
14
+ '%',
15
+ 'px',
16
+ 'rpx',
17
+ 'rem',
18
+ 'em',
19
+ 'vw',
20
+ 'vh',
21
+ 'vmin',
22
+ 'vmax',
23
+ 'dvw',
24
+ 'dvh',
25
+ 'svw',
26
+ 'svh',
27
+ 'lvw',
28
+ 'lvh',
29
+ 'ch',
30
+ 'ex',
31
+ 'lh',
32
+ 'rlh',
33
+ 'fr',
34
+ 'deg',
35
+ 'rad',
36
+ 'turn',
37
+ 's',
38
+ 'ms',
39
+ ]
40
+
41
+ const NUMBER_RE = /^-?(?:\d+|\d*\.\d+)$/
42
+ const FUNCTION_VALUE_RE = /^[a-zA-Z_-][a-zA-Z0-9_-]*\(/
43
+
44
+ function splitVariantPrefix(candidate: string) {
45
+ let depth = 0
46
+ let quote: string | undefined
47
+ let lastSeparator = -1
48
+
49
+ for (let index = 0; index < candidate.length; index++) {
50
+ const character = candidate[index]
51
+ if (character === '\\') {
52
+ index++
53
+ continue
54
+ }
55
+
56
+ if (quote) {
57
+ if (character === quote) {
58
+ quote = undefined
59
+ }
60
+ continue
61
+ }
62
+
63
+ if (character === '"' || character === '\'') {
64
+ quote = character
65
+ continue
66
+ }
67
+
68
+ if (character === '[' || character === '(' || character === '{') {
69
+ depth++
70
+ continue
71
+ }
72
+
73
+ if (character === ']' || character === ')' || character === '}') {
74
+ depth = Math.max(0, depth - 1)
75
+ continue
76
+ }
77
+
78
+ if (depth === 0 && character === ':') {
79
+ lastSeparator = index
80
+ }
81
+ }
82
+
83
+ if (lastSeparator === -1) {
84
+ return {
85
+ prefix: '',
86
+ body: candidate,
87
+ }
88
+ }
89
+
90
+ return {
91
+ prefix: candidate.slice(0, lastSeparator + 1),
92
+ body: candidate.slice(lastSeparator + 1),
93
+ }
94
+ }
95
+
96
+ function isBalancedFunctionValue(value: string) {
97
+ let depth = 0
98
+ let quote: string | undefined
99
+
100
+ for (let index = 0; index < value.length; index++) {
101
+ const character = value[index]
102
+
103
+ if (character === '\\') {
104
+ index++
105
+ continue
106
+ }
107
+
108
+ if (quote) {
109
+ if (character === quote) {
110
+ quote = undefined
111
+ }
112
+ continue
113
+ }
114
+
115
+ if (character === '"' || character === '\'') {
116
+ quote = character
117
+ continue
118
+ }
119
+
120
+ if (character === '(') {
121
+ depth++
122
+ continue
123
+ }
124
+
125
+ if (character === ')') {
126
+ depth--
127
+ if (depth < 0) {
128
+ return false
129
+ }
130
+ }
131
+ }
132
+
133
+ return depth === 0 && quote === undefined
134
+ }
135
+
136
+ function isHexColorValue(value: string) {
137
+ return /^#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6,8})$/.test(value)
138
+ }
139
+
140
+ function isQuotedValue(value: string) {
141
+ const quote = value[0]
142
+ if ((quote !== '"' && quote !== '\'') || value[value.length - 1] !== quote) {
143
+ return false
144
+ }
145
+
146
+ let escaped = false
147
+ for (let index = 1; index < value.length - 1; index++) {
148
+ const character = value[index]
149
+ if (escaped) {
150
+ escaped = false
151
+ continue
152
+ }
153
+ if (character === '\\') {
154
+ escaped = true
155
+ }
156
+ }
157
+
158
+ return !escaped
159
+ }
160
+
161
+ function normalizeBareArbitraryValueOptions(options: boolean | BareArbitraryValueOptions | undefined) {
162
+ if (options === false || options === undefined || options === null) {
163
+ return
164
+ }
165
+
166
+ const units = options === true ? DEFAULT_BARE_ARBITRARY_VALUE_UNITS : options.units ?? DEFAULT_BARE_ARBITRARY_VALUE_UNITS
167
+ const normalizedUnits = [...new Set(units.filter(unit => typeof unit === 'string' && unit.length > 0))]
168
+ if (normalizedUnits.length === 0) {
169
+ return
170
+ }
171
+ return {
172
+ units: normalizedUnits.sort((a, b) => b.length - a.length),
173
+ }
174
+ }
175
+
176
+ function resolveValueWithUnit(body: string, units: string[]) {
177
+ for (const unit of units) {
178
+ if (!body.endsWith(unit)) {
179
+ continue
180
+ }
181
+ const numberPart = body.slice(0, -unit.length)
182
+ if (NUMBER_RE.test(numberPart)) {
183
+ return `${numberPart}${unit}`
184
+ }
185
+ }
186
+ }
187
+
188
+ function resolveArbitraryValue(body: string, units: string[]) {
189
+ const withUnit = resolveValueWithUnit(body, units)
190
+ if (withUnit) {
191
+ return withUnit
192
+ }
193
+
194
+ if (isHexColorValue(body)) {
195
+ return body
196
+ }
197
+
198
+ if (isQuotedValue(body)) {
199
+ return body
200
+ }
201
+
202
+ if (FUNCTION_VALUE_RE.test(body) && body.endsWith(')') && isBalancedFunctionValue(body)) {
203
+ return body
204
+ }
205
+ }
206
+
207
+ function resolveUtilityAndValue(body: string, units: string[]) {
208
+ let depth = 0
209
+ let quote: string | undefined
210
+
211
+ for (let index = 1; index < body.length; index++) {
212
+ const character = body[index]
213
+
214
+ if (character === '\\') {
215
+ index++
216
+ continue
217
+ }
218
+
219
+ if (quote) {
220
+ if (character === quote) {
221
+ quote = undefined
222
+ }
223
+ continue
224
+ }
225
+
226
+ if (character === '"' || character === '\'') {
227
+ quote = character
228
+ continue
229
+ }
230
+
231
+ if (character === '(' || character === '{') {
232
+ depth++
233
+ continue
234
+ }
235
+
236
+ if (character === ')' || character === '}') {
237
+ depth = Math.max(0, depth - 1)
238
+ continue
239
+ }
240
+
241
+ if (depth > 0 || character !== '-') {
242
+ continue
243
+ }
244
+
245
+ const utility = body.slice(0, index)
246
+ const rawValue = body.slice(index + 1)
247
+ if (!utility || !rawValue) {
248
+ continue
249
+ }
250
+
251
+ const value = resolveArbitraryValue(rawValue, units)
252
+ if (value) {
253
+ return {
254
+ utility,
255
+ value,
256
+ }
257
+ }
258
+ }
259
+ }
260
+
261
+ export function resolveBareArbitraryValueCandidate(
262
+ candidate: string,
263
+ options?: boolean | BareArbitraryValueOptions,
264
+ ): BareArbitraryValueResolveResult | undefined {
265
+ const normalizedOptions = normalizeBareArbitraryValueOptions(options)
266
+ if (!normalizedOptions || !candidate || candidate.includes('[') || candidate.includes(']')) {
267
+ return
268
+ }
269
+
270
+ const { prefix, body } = splitVariantPrefix(candidate)
271
+ const important = body.startsWith('!') ? '!' : ''
272
+ let normalizedBody = important ? body.slice(1) : body
273
+ const negative = normalizedBody.startsWith('-') ? '-' : ''
274
+ if (negative) {
275
+ normalizedBody = normalizedBody.slice(1)
276
+ }
277
+
278
+ const resolved = resolveUtilityAndValue(normalizedBody, normalizedOptions.units)
279
+ if (!resolved) {
280
+ return
281
+ }
282
+
283
+ return {
284
+ candidate,
285
+ canonicalCandidate: `${prefix}${important}${negative}${resolved.utility}-[${resolved.value}]`,
286
+ }
287
+ }
288
+
289
+ // Based on the CSS.escape algorithm, scoped to class selector escaping.
290
+ export function escapeCssClassName(value: string) {
291
+ let result = ''
292
+ for (let index = 0; index < value.length; index++) {
293
+ const codeUnit = value.charCodeAt(index)
294
+ const character = value.charAt(index)
295
+
296
+ if (codeUnit === 0x0000) {
297
+ result += '\uFFFD'
298
+ continue
299
+ }
300
+
301
+ if (
302
+ (codeUnit >= 0x0001 && codeUnit <= 0x001F)
303
+ || codeUnit === 0x007F
304
+ || (index === 0 && codeUnit >= 0x0030 && codeUnit <= 0x0039)
305
+ || (index === 1 && codeUnit >= 0x0030 && codeUnit <= 0x0039 && value.charCodeAt(0) === 0x002D)
306
+ ) {
307
+ result += `\\${codeUnit.toString(16)} `
308
+ continue
309
+ }
310
+
311
+ if (
312
+ codeUnit >= 0x0080
313
+ || codeUnit === 0x002D
314
+ || codeUnit === 0x005F
315
+ || (codeUnit >= 0x0030 && codeUnit <= 0x0039)
316
+ || (codeUnit >= 0x0041 && codeUnit <= 0x005A)
317
+ || (codeUnit >= 0x0061 && codeUnit <= 0x007A)
318
+ ) {
319
+ result += character
320
+ continue
321
+ }
322
+
323
+ result += `\\${character}`
324
+ }
325
+ return result
326
+ }
@@ -1,20 +1,39 @@
1
1
  import type { TailwindV4DesignSystem } from './types'
2
2
  import postcss from 'postcss'
3
+ import {
4
+ escapeCssClassName,
5
+ type BareArbitraryValueOptions,
6
+ resolveBareArbitraryValueCandidate,
7
+ } from './bare-arbitrary-values'
3
8
 
4
9
  export function resolveValidTailwindV4Candidates(
5
10
  designSystem: TailwindV4DesignSystem,
6
11
  candidates: Iterable<string>,
12
+ options?: {
13
+ bareArbitraryValues?: boolean | BareArbitraryValueOptions
14
+ },
7
15
  ): Set<string> {
8
16
  const validCandidates = new Set<string>()
9
17
  const parsedCandidates: string[] = []
18
+ const originalCandidateByCanonical = new Map<string, string>()
10
19
 
11
20
  for (const candidate of candidates) {
12
- if (!candidate || parsedCandidates.includes(candidate)) {
21
+ if (!candidate) {
13
22
  continue
14
23
  }
15
24
 
16
- if (designSystem.parseCandidate(candidate).length > 0) {
17
- parsedCandidates.push(candidate)
25
+ const bareArbitrary = resolveBareArbitraryValueCandidate(candidate, options?.bareArbitraryValues)
26
+ const candidateToCheck = bareArbitrary?.canonicalCandidate ?? candidate
27
+
28
+ if (parsedCandidates.includes(candidateToCheck)) {
29
+ continue
30
+ }
31
+
32
+ if (designSystem.parseCandidate(candidateToCheck).length > 0) {
33
+ parsedCandidates.push(candidateToCheck)
34
+ if (bareArbitrary) {
35
+ originalCandidateByCanonical.set(candidateToCheck, candidate)
36
+ }
18
37
  }
19
38
  }
20
39
 
@@ -27,13 +46,58 @@ export function resolveValidTailwindV4Candidates(
27
46
  const candidate = parsedCandidates[index]
28
47
  const candidateCss = cssByCandidate[index]
29
48
  if (candidate && typeof candidateCss === 'string' && candidateCss.trim().length > 0) {
30
- validCandidates.add(candidate)
49
+ validCandidates.add(originalCandidateByCanonical.get(candidate) ?? candidate)
31
50
  }
32
51
  }
33
52
 
34
53
  return validCandidates
35
54
  }
36
55
 
56
+ function createSelectorAliasMap(
57
+ candidates: Iterable<string>,
58
+ options?: boolean | BareArbitraryValueOptions,
59
+ ) {
60
+ const aliases = new Map<string, string>()
61
+ for (const candidate of candidates) {
62
+ const bareArbitrary = resolveBareArbitraryValueCandidate(candidate, options)
63
+ if (!bareArbitrary) {
64
+ continue
65
+ }
66
+ aliases.set(
67
+ escapeCssClassName(bareArbitrary.canonicalCandidate),
68
+ escapeCssClassName(bareArbitrary.candidate),
69
+ )
70
+ }
71
+ return aliases
72
+ }
73
+
74
+ export function replaceBareArbitraryValueSelectors(
75
+ css: string,
76
+ candidates: Iterable<string>,
77
+ options?: boolean | BareArbitraryValueOptions,
78
+ ) {
79
+ const aliases = createSelectorAliasMap(candidates, options)
80
+ if (aliases.size === 0) {
81
+ return css
82
+ }
83
+
84
+ let result = css
85
+ for (const [canonicalSelector, bareSelector] of aliases) {
86
+ result = result.replaceAll(canonicalSelector, bareSelector)
87
+ }
88
+ return result
89
+ }
90
+
91
+ export function canonicalizeBareArbitraryValueCandidates(
92
+ candidates: Iterable<string>,
93
+ options?: boolean | BareArbitraryValueOptions,
94
+ ) {
95
+ return Array.from(candidates, (candidate) => {
96
+ const bareArbitrary = resolveBareArbitraryValueCandidate(candidate, options)
97
+ return bareArbitrary?.canonicalCandidate ?? candidate
98
+ })
99
+ }
100
+
37
101
  function splitTopLevel(value: string, separator: string) {
38
102
  const result: string[] = []
39
103
  let start = 0
package/src/v4/engine.ts CHANGED
@@ -6,7 +6,12 @@ import type {
6
6
  TailwindV4SourcePattern,
7
7
  } from './types'
8
8
  import { extractRawCandidates, extractRawCandidatesWithPositions } from '../extraction/candidate-extractor'
9
- import { extractTailwindV4InlineSourceCandidates, resolveValidTailwindV4Candidates } from './candidates'
9
+ import {
10
+ canonicalizeBareArbitraryValueCandidates,
11
+ extractTailwindV4InlineSourceCandidates,
12
+ replaceBareArbitraryValueSelectors,
13
+ resolveValidTailwindV4Candidates,
14
+ } from './candidates'
10
15
  import { compileTailwindV4Source, loadTailwindV4DesignSystem } from './node-adapter'
11
16
 
12
17
  function resolveScanSources(
@@ -72,13 +77,20 @@ export function createTailwindV4Engine(source: TailwindV4ResolvedSource): Tailwi
72
77
  const { compiled, dependencies } = await compileTailwindV4Source(source)
73
78
  const rawCandidates = await collectRawCandidates(source, options, compiled.sources)
74
79
  const designSystem = await loadTailwindV4DesignSystem(source)
75
- const classSet = resolveValidTailwindV4Candidates(designSystem, rawCandidates)
80
+ const classSet = resolveValidTailwindV4Candidates(designSystem, rawCandidates, {
81
+ ...(options?.bareArbitraryValues === undefined ? {} : { bareArbitraryValues: options.bareArbitraryValues }),
82
+ })
76
83
  const inlineSources = extractTailwindV4InlineSourceCandidates(source.css)
77
84
  for (const candidate of inlineSources.excluded) {
78
85
  classSet.delete(candidate)
79
86
  }
80
87
 
81
- const css = compiled.build(Array.from(classSet))
88
+ const buildCandidates = canonicalizeBareArbitraryValueCandidates(classSet, options?.bareArbitraryValues)
89
+ const css = replaceBareArbitraryValueSelectors(
90
+ compiled.build(buildCandidates),
91
+ classSet,
92
+ options?.bareArbitraryValues,
93
+ )
82
94
 
83
95
  return {
84
96
  css,
package/src/v4/types.ts CHANGED
@@ -24,6 +24,12 @@ export interface TailwindV4CandidateSource {
24
24
  export interface TailwindV4GenerateOptions {
25
25
  candidates?: Iterable<string>
26
26
  sources?: TailwindV4CandidateSource[]
27
+ /**
28
+ * Enables UnoCSS-style bare arbitrary values such as `p-10%` and `p-2.5px`.
29
+ */
30
+ bareArbitraryValues?: boolean | {
31
+ units?: string[]
32
+ }
27
33
  /**
28
34
  * 扫描文件系统 source entries 中的候选类名。
29
35
  *