purgetss 7.7.0 → 7.9.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.
- package/README.md +11 -1
- package/bin/purgetss +24 -7
- package/dist/purgetss.ui.js +1 -1
- package/lib/templates/purgetss.config.js.cjs +6 -15
- package/package.json +2 -2
- package/src/cli/commands/build.js +9 -4
- package/src/cli/commands/images.js +8 -0
- package/src/cli/commands/purge.js +17 -3
- package/src/cli/commands/shades.js +2 -2
- package/src/cli/utils/unsupported-class-reporter.js +209 -0
- package/src/core/branding/ensure-brand-section.js +6 -15
- package/src/core/branding/post-gen-notes.js +12 -4
- package/{experimental/completions2.js → src/core/builders/auto-utilities-builder.js} +56 -27
- package/src/core/builders/tailwind-builder.js +2 -2
- package/src/core/builders/tailwind-helpers.js +0 -444
- package/src/core/images/gen-scales.js +24 -6
- package/src/core/images/index.js +9 -2
- package/src/core/purger/tailwind-purger.js +40 -4
- package/src/shared/helpers/core.js +0 -19
- package/src/shared/helpers/utils.js +100 -28
- package/src/shared/semantic-helpers.js +143 -0
- package/src/dev/builders/tailwind-builder.js +0 -26
|
@@ -73,6 +73,26 @@ export const IPHONE_SCALES = Object.freeze([
|
|
|
73
73
|
{ suffix: '@3x', factor: 3 / 4 }
|
|
74
74
|
])
|
|
75
75
|
|
|
76
|
+
// Resolve target dimensions for a single scale.
|
|
77
|
+
// `factor * 4` recovers the integer multiplier (1, 1.5, 2, 3, 4 for Android;
|
|
78
|
+
// 1, 2, 3 for iPhone) only because every entry in *_SCALES is normalized to
|
|
79
|
+
// n/4 with the largest scale (xxxhdpi/@4x) at 4/4. If a future density is
|
|
80
|
+
// added beyond xxxhdpi, this conversion factor needs to be revisited.
|
|
81
|
+
function computeScaleTarget(srcMeta, factor, baseWidth) {
|
|
82
|
+
if (baseWidth == null) {
|
|
83
|
+
return {
|
|
84
|
+
targetWidth: Math.max(1, Math.round(srcMeta.width * factor)),
|
|
85
|
+
targetHeight: Math.max(1, Math.round(srcMeta.height * factor))
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const multiplier = factor * 4
|
|
89
|
+
const aspect = srcMeta.width > 0 ? (srcMeta.height / srcMeta.width) : 1
|
|
90
|
+
return {
|
|
91
|
+
targetWidth: Math.max(1, Math.round(baseWidth * multiplier)),
|
|
92
|
+
targetHeight: Math.max(1, Math.round(baseWidth * multiplier * aspect))
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
76
96
|
/**
|
|
77
97
|
* Scale a source image into all Android density variants.
|
|
78
98
|
*
|
|
@@ -85,13 +105,12 @@ export const IPHONE_SCALES = Object.freeze([
|
|
|
85
105
|
* @returns {Promise<string[]>} Paths written
|
|
86
106
|
*/
|
|
87
107
|
export async function genAndroidScales(sourceFile, relPath, androidBaseDir, opts = {}) {
|
|
88
|
-
const { format = null, quality = 85 } = opts
|
|
108
|
+
const { format = null, quality = 85, baseWidth = null } = opts
|
|
89
109
|
const src = await readSource(sourceFile)
|
|
90
110
|
const written = []
|
|
91
111
|
|
|
92
112
|
for (const { name, factor } of ANDROID_SCALES) {
|
|
93
|
-
const targetWidth =
|
|
94
|
-
const targetHeight = Math.max(1, Math.round(src.meta.height * factor))
|
|
113
|
+
const { targetWidth, targetHeight } = computeScaleTarget(src.meta, factor, baseWidth)
|
|
95
114
|
|
|
96
115
|
const outDir = path.join(androidBaseDir, name, path.dirname(relPath))
|
|
97
116
|
fs.mkdirSync(outDir, { recursive: true })
|
|
@@ -113,7 +132,7 @@ export async function genAndroidScales(sourceFile, relPath, androidBaseDir, opts
|
|
|
113
132
|
* @returns {Promise<string[]>} Paths written
|
|
114
133
|
*/
|
|
115
134
|
export async function genIphoneScales(sourceFile, relPath, iphoneBaseDir, opts = {}) {
|
|
116
|
-
const { format = null, quality = 85 } = opts
|
|
135
|
+
const { format = null, quality = 85, baseWidth = null } = opts
|
|
117
136
|
const src = await readSource(sourceFile)
|
|
118
137
|
const written = []
|
|
119
138
|
|
|
@@ -122,8 +141,7 @@ export async function genIphoneScales(sourceFile, relPath, iphoneBaseDir, opts =
|
|
|
122
141
|
fs.mkdirSync(outDir, { recursive: true })
|
|
123
142
|
|
|
124
143
|
for (const { suffix, factor } of IPHONE_SCALES) {
|
|
125
|
-
const targetWidth =
|
|
126
|
-
const targetHeight = Math.max(1, Math.round(src.meta.height * factor))
|
|
144
|
+
const { targetWidth, targetHeight } = computeScaleTarget(src.meta, factor, baseWidth)
|
|
127
145
|
|
|
128
146
|
// SVG sources can't be written as SVG by Sharp — fall back to PNG if the
|
|
129
147
|
// user didn't specify an explicit output format.
|
package/src/core/images/index.js
CHANGED
|
@@ -35,6 +35,7 @@ export async function runImages(opts) {
|
|
|
35
35
|
iphoneOnly = false,
|
|
36
36
|
format = null,
|
|
37
37
|
quality = 85,
|
|
38
|
+
baseWidth = null,
|
|
38
39
|
dryRun = false,
|
|
39
40
|
yes = false,
|
|
40
41
|
confirmOverwrites = true
|
|
@@ -49,6 +50,11 @@ export async function runImages(opts) {
|
|
|
49
50
|
|
|
50
51
|
const files = collectImageFiles(source)
|
|
51
52
|
|
|
53
|
+
if (baseWidth == null && files.some(f => path.extname(f).toLowerCase() === '.svg')) {
|
|
54
|
+
logger.warning('⚠ SVG source detected without --width. Output sizes will be derived from each SVG\'s viewBox (treated as a 4× master).')
|
|
55
|
+
logger.warning(' For SVGs from vector editors with disproportionate viewBoxes, pass --width <n> (e.g. --width 256) to pin the @1x/mdpi width.')
|
|
56
|
+
}
|
|
57
|
+
|
|
52
58
|
console.log()
|
|
53
59
|
mainLogger.info('Generating multi-density image variants...')
|
|
54
60
|
console.log()
|
|
@@ -60,6 +66,7 @@ export async function runImages(opts) {
|
|
|
60
66
|
if (!androidOnly) platforms.push('iPhone (@1x, @2x, @3x)')
|
|
61
67
|
logger.property('Platforms: ', platforms.join(' + '))
|
|
62
68
|
if (format) logger.property('Format: ', `convert all to ${format}`)
|
|
69
|
+
if (baseWidth != null) logger.property('Width: ', `${baseWidth} px @1x/mdpi`)
|
|
63
70
|
if (dryRun) logger.warning('DRY RUN — no files will be written')
|
|
64
71
|
|
|
65
72
|
if (files.length === 0) {
|
|
@@ -115,11 +122,11 @@ export async function runImages(opts) {
|
|
|
115
122
|
if (dryRun) continue
|
|
116
123
|
|
|
117
124
|
if (!iphoneOnly) {
|
|
118
|
-
const androidFiles = await genAndroidScales(file, relPath, androidBaseDir, { format, quality })
|
|
125
|
+
const androidFiles = await genAndroidScales(file, relPath, androidBaseDir, { format, quality, baseWidth })
|
|
119
126
|
written.push(...androidFiles)
|
|
120
127
|
}
|
|
121
128
|
if (!androidOnly) {
|
|
122
|
-
const iphoneFiles = await genIphoneScales(file, relPath, iphoneBaseDir, { format, quality })
|
|
129
|
+
const iphoneFiles = await genIphoneScales(file, relPath, iphoneBaseDir, { format, quality, baseWidth })
|
|
123
130
|
written.push(...iphoneFiles)
|
|
124
131
|
}
|
|
125
132
|
}
|
|
@@ -14,6 +14,7 @@ import _ from 'lodash'
|
|
|
14
14
|
import chalk from 'chalk'
|
|
15
15
|
import * as helpers from '../../shared/helpers.js'
|
|
16
16
|
import { logger } from '../../shared/logger.js'
|
|
17
|
+
import { deriveAlphaKey } from '../../shared/semantic-helpers.js'
|
|
17
18
|
import {
|
|
18
19
|
// eslint-disable-next-line camelcase
|
|
19
20
|
projectsTailwind_TSS,
|
|
@@ -191,10 +192,20 @@ export function purgeTailwind(uniqueClasses, debug = false) {
|
|
|
191
192
|
const opacityIndex = _.findIndex(tailwindClasses, line => line.startsWith(`'.${opacityValue.className}'`))
|
|
192
193
|
|
|
193
194
|
const classProperties = tailwindClasses[opacityIndex]
|
|
194
|
-
if (opacityIndex > -1 && classProperties.includes('#')) {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
195
|
+
if (opacityIndex > -1 && classProperties && !classProperties.includes('#')) {
|
|
196
|
+
const derivedLine = tryDeriveSemanticOpacityLine(classProperties, opacityValue)
|
|
197
|
+
if (derivedLine) {
|
|
198
|
+
purgedClasses += switchPlatform(helpers.checkPlatformAndDevice(derivedLine, opacityValue.classNameWithTransparency))
|
|
199
|
+
} else {
|
|
200
|
+
console.warn('')
|
|
201
|
+
console.warn(chalk.yellow(` Skipping ".${opacityValue.className}/${opacityValue.decimalValue}" — semantic color, no hex to blend.`))
|
|
202
|
+
console.warn(chalk.yellow(` Use a PurgeTSS built-in color, bg-(#AARRGGBB), or "purgetss semantic --single ... --alpha ${opacityValue.decimalValue}".`))
|
|
203
|
+
console.warn('')
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (opacityIndex > -1 && classProperties && classProperties.includes('#')) {
|
|
207
|
+
const hexMatches = classProperties.match(/#[0-9a-f]{6}/gi)
|
|
208
|
+
const defaultHexValue = (classProperties.includes('from')) ? hexMatches[1] : hexMatches[0]
|
|
198
209
|
let classWithoutDecimalOpacity = `${classProperties.replace(new RegExp(defaultHexValue.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), `#${opacityValue.transparency}${defaultHexValue.substring(1)}`)}`
|
|
199
210
|
// Special case: #000000
|
|
200
211
|
if (classProperties.includes('from') && defaultHexValue === '#000000') classWithoutDecimalOpacity = classWithoutDecimalOpacity.replace('00000000', '000000')
|
|
@@ -216,6 +227,31 @@ export function purgeTailwind(uniqueClasses, debug = false) {
|
|
|
216
227
|
return purgedClasses
|
|
217
228
|
}
|
|
218
229
|
|
|
230
|
+
// Auto-derive a semantic key with applied alpha and emit a TSS line for the
|
|
231
|
+
// `class/N` form. Returns the rewritten line (with selector renamed to include
|
|
232
|
+
// `/N` and the semantic value swapped for the derived key), or `null` when no
|
|
233
|
+
// candidate matches an entry in semantic.colors.json. Conflict errors from
|
|
234
|
+
// `deriveAlphaKey` propagate naturally.
|
|
235
|
+
function tryDeriveSemanticOpacityLine(classProperties, opacityValue) {
|
|
236
|
+
const bodyMatch = classProperties.match(/\{([^}]*)\}/)
|
|
237
|
+
if (!bodyMatch) return null
|
|
238
|
+
const candidates = (bodyMatch[1].match(/'([^']+)'/g) || [])
|
|
239
|
+
.map(m => m.slice(1, -1))
|
|
240
|
+
.filter(v => !v.startsWith('#'))
|
|
241
|
+
for (const candidate of candidates) {
|
|
242
|
+
const derivedKey = deriveAlphaKey(candidate, opacityValue.decimalValue)
|
|
243
|
+
if (derivedKey) {
|
|
244
|
+
let line = classProperties.replace(new RegExp(`'${candidate}'`, 'g'), `'${derivedKey}'`)
|
|
245
|
+
line = line.replace(
|
|
246
|
+
`'.${opacityValue.className}'`,
|
|
247
|
+
`'.${opacityValue.className}/${opacityValue.decimalValue}'`
|
|
248
|
+
)
|
|
249
|
+
return line
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return null
|
|
253
|
+
}
|
|
254
|
+
|
|
219
255
|
/**
|
|
220
256
|
* Switch platform specific styles - COPIED exactly from original switchPlatform() function
|
|
221
257
|
* NO CHANGES to logic, preserving 100% of original functionality
|
|
@@ -1,21 +1,2 @@
|
|
|
1
|
-
// Import customRules function for resetStyles
|
|
2
|
-
import { customRules } from './utils.js'
|
|
3
|
-
|
|
4
1
|
// Global configurations
|
|
5
2
|
export const globalOptions = {}
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Reset styles for common Titanium components
|
|
9
|
-
* Applies default styles to ImageView, View, and Window
|
|
10
|
-
* @returns {string} Generated reset styles
|
|
11
|
-
*/
|
|
12
|
-
export function resetStyles() {
|
|
13
|
-
let convertedStyles = '\n// Custom Styles and Resets\n'
|
|
14
|
-
|
|
15
|
-
convertedStyles += customRules({ ios: { hires: true } }, 'ImageView')
|
|
16
|
-
// convertedStyles += customRules({ default: { width: 'Ti.UI.FILL', height: 'Ti.UI.SIZE' } }, 'Label');
|
|
17
|
-
convertedStyles += customRules({ default: { width: 'Ti.UI.SIZE', height: 'Ti.UI.SIZE' } }, 'View')
|
|
18
|
-
convertedStyles += customRules({ default: { backgroundColor: '#ffffff' } }, 'Window')
|
|
19
|
-
|
|
20
|
-
return convertedStyles
|
|
21
|
-
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'fs'
|
|
2
2
|
import _ from 'lodash'
|
|
3
|
+
import { deriveAlphaKey } from '../semantic-helpers.js'
|
|
3
4
|
|
|
4
5
|
// Internal variables and constants
|
|
5
6
|
const _applyClasses = {}
|
|
@@ -530,15 +531,38 @@ export function compileApplyDirectives(twClasses) {
|
|
|
530
531
|
const opacityIndex = findIndexOfClassName(`'.${opacityValue.classNameWithTransparency}`, twClassesArray)
|
|
531
532
|
|
|
532
533
|
if (opacityIndex > -1) {
|
|
533
|
-
const
|
|
534
|
-
const
|
|
534
|
+
const targetLine = twClassesArray[opacityIndex]
|
|
535
|
+
const hexMatches = targetLine.match(/#[0-9a-f]{6}/gi)
|
|
536
|
+
|
|
537
|
+
if (!hexMatches || (targetLine.includes('from') && hexMatches.length < 2)) {
|
|
538
|
+
const derivedLine = tryDeriveSemanticOpacity(targetLine, opacityValue.decimalValue)
|
|
539
|
+
if (derivedLine) {
|
|
540
|
+
compoundClasses.push(justProperties(derivedLine))
|
|
541
|
+
return
|
|
542
|
+
}
|
|
543
|
+
throw new Error(
|
|
544
|
+
`Opacity "/${opacityValue.decimalValue}" can't apply to semantic color ".${opacityValue.classNameWithTransparency}" (in apply of "${className}"). Use a PurgeTSS built-in color, an arbitrary value bg-(#AARRGGBB), or set alpha via "purgetss semantic --single ... --alpha ${opacityValue.decimalValue}".`
|
|
545
|
+
)
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const defaultHexValue = targetLine.includes('from') ? hexMatches[1] : hexMatches[0]
|
|
549
|
+
const classWithoutDecimalOpacity = `${targetLine.replace(new RegExp(defaultHexValue.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), `#${opacityValue.transparency}${defaultHexValue.substring(1)}`)}`
|
|
535
550
|
|
|
536
551
|
compoundClasses.push(justProperties(classWithoutDecimalOpacity))
|
|
537
552
|
}
|
|
538
553
|
})
|
|
539
554
|
}
|
|
540
555
|
|
|
541
|
-
|
|
556
|
+
let mergedProperties
|
|
557
|
+
try {
|
|
558
|
+
mergedProperties = fixDuplicateKeys(compoundClasses).join(', ')
|
|
559
|
+
} catch (mergeError) {
|
|
560
|
+
const classList = [...values].join(' ')
|
|
561
|
+
throw new Error(
|
|
562
|
+
`Failed to merge apply directive of "${className}".\n Classes: "${classList}".\n Hint: this usually means an unsupported combination — e.g. "bg-gradient-to-X" together with "from-X to-Y" in the same apply (gradient direction is dropped on merge), or two utilities that map to the same property in incompatible shapes. Try splitting the rule or removing one of the conflicting classes.\n Internal: ${mergeError.message}`
|
|
563
|
+
)
|
|
564
|
+
}
|
|
565
|
+
twClassesArray[indexOfModifier] = _.replace(twClassesArray[indexOfModifier], /{_applyProperties_}/, mergedProperties)
|
|
542
566
|
twClassesArray[indexOfModifier] = deduplicateLineProperties(twClassesArray[indexOfModifier])
|
|
543
567
|
}
|
|
544
568
|
})
|
|
@@ -546,6 +570,26 @@ export function compileApplyDirectives(twClasses) {
|
|
|
546
570
|
return twClassesArray.join('\n')
|
|
547
571
|
}
|
|
548
572
|
|
|
573
|
+
// Attempt to derive an alpha-applied semantic key from a TSS line whose value
|
|
574
|
+
// is a semantic name (e.g. `'.bg-surface': { backgroundColor: 'surfaceColor' }`).
|
|
575
|
+
// Returns the rewritten line with the derived key in place of the base name,
|
|
576
|
+
// or `null` when no candidate matches an entry in semantic.colors.json.
|
|
577
|
+
// Conflict errors from `deriveAlphaKey` propagate naturally.
|
|
578
|
+
function tryDeriveSemanticOpacity(targetLine, alphaPercent) {
|
|
579
|
+
const bodyMatch = targetLine.match(/\{([^}]*)\}/)
|
|
580
|
+
if (!bodyMatch) return null
|
|
581
|
+
const candidates = (bodyMatch[1].match(/'([^']+)'/g) || [])
|
|
582
|
+
.map(m => m.slice(1, -1))
|
|
583
|
+
.filter(v => !v.startsWith('#'))
|
|
584
|
+
for (const candidate of candidates) {
|
|
585
|
+
const derivedKey = deriveAlphaKey(candidate, alphaPercent)
|
|
586
|
+
if (derivedKey) {
|
|
587
|
+
return targetLine.replace(new RegExp(`'${candidate}'`, 'g'), `'${derivedKey}'`)
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
return null
|
|
591
|
+
}
|
|
592
|
+
|
|
549
593
|
/**
|
|
550
594
|
* Remove duplicate property keys in a TSS line, keeping the last occurrence.
|
|
551
595
|
* This ensures apply directives override static defaults (e.g. backgroundColor).
|
|
@@ -593,13 +637,20 @@ export function justProperties(_foundClass) {
|
|
|
593
637
|
|
|
594
638
|
export function formatArbitraryValues(arbitraryValue, fromXMLs = false) {
|
|
595
639
|
const sign = (arbitraryValue.startsWith('-')) ? '-' : ''
|
|
596
|
-
const
|
|
640
|
+
const stripped = sign ? arbitraryValue.substring(1) : arbitraryValue
|
|
597
641
|
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
642
|
+
// Extract the value inside the last (...) so a negative value like top-(-10)
|
|
643
|
+
// is not mis-split by the hyphen inside the parentheses.
|
|
644
|
+
const parenMatch = stripped.match(/^(.+)-\(([^()]*)\)$/)
|
|
645
|
+
if (!parenMatch || parenMatch[2].trim() === '') {
|
|
646
|
+
return (fromXMLs) ? `// Property not yet supported: ${arbitraryValue}` : null
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const ruleParts = parenMatch[1].split('-')
|
|
650
|
+
let value = parenMatch[2]
|
|
651
|
+
|
|
652
|
+
if (ruleParts.length === 1) {
|
|
653
|
+
let rule = ruleParts[0]
|
|
603
654
|
|
|
604
655
|
if (rule === 'text') {
|
|
605
656
|
rule = (value.includes('#') || value.includes('rgb')) ? 'text-color' : 'text-size'
|
|
@@ -630,13 +681,12 @@ export function formatArbitraryValues(arbitraryValue, fromXMLs = false) {
|
|
|
630
681
|
? `'.${arbitraryValue}': { ` + _.replace(properties, /{value}/g, parseValue(value, sign)) + ' }'
|
|
631
682
|
: _.replace(properties, /{value}/g, parseValue(value, sign))
|
|
632
683
|
}
|
|
633
|
-
} else if (
|
|
634
|
-
const rule =
|
|
635
|
-
const value = splitedContent[2].match(/(?<=\().*(?=\))/).pop()
|
|
684
|
+
} else if (ruleParts.length === 2) {
|
|
685
|
+
const rule = ruleParts.join('-')
|
|
636
686
|
let properties = arbitraryValuesTable[rule]
|
|
637
687
|
|
|
638
688
|
if (properties) {
|
|
639
|
-
if (
|
|
689
|
+
if (ruleParts[0] === 'rounded') {
|
|
640
690
|
if (!value.includes(',')) {
|
|
641
691
|
properties = _.replace(properties, /{value1}/g, parseValue(parseValue(value) / 2, sign))
|
|
642
692
|
} else {
|
|
@@ -651,9 +701,8 @@ export function formatArbitraryValues(arbitraryValue, fromXMLs = false) {
|
|
|
651
701
|
? `'.${arbitraryValue}': { ` + _.replace(properties, /{value}/g, parseValue(value, sign)) + ' }'
|
|
652
702
|
: _.replace(properties, /{value}/g, parseValue(value, sign))
|
|
653
703
|
}
|
|
654
|
-
} else if (
|
|
655
|
-
const rule =
|
|
656
|
-
const value = splitedContent[3].match(/(?<=\().*(?=\))/).pop()
|
|
704
|
+
} else if (ruleParts.length === 3) {
|
|
705
|
+
const rule = ruleParts.join('-')
|
|
657
706
|
let properties = arbitraryValuesTable[rule]
|
|
658
707
|
|
|
659
708
|
if (properties) {
|
|
@@ -665,9 +714,8 @@ export function formatArbitraryValues(arbitraryValue, fromXMLs = false) {
|
|
|
665
714
|
? `'.${arbitraryValue}': { ` + _.replace(properties, /{value}/g, parseValue(value, sign)) + ' }'
|
|
666
715
|
: _.replace(properties, /{value}/g, parseValue(value, sign))
|
|
667
716
|
}
|
|
668
|
-
} else if (
|
|
669
|
-
const rule =
|
|
670
|
-
const value = splitedContent[4].match(/(?<=\().*(?=\))/).pop()
|
|
717
|
+
} else if (ruleParts.length === 4) {
|
|
718
|
+
const rule = ruleParts.join('-')
|
|
671
719
|
const properties = arbitraryValuesTable[rule]
|
|
672
720
|
|
|
673
721
|
if (properties) {
|
|
@@ -726,13 +774,22 @@ export function fixDuplicateKeys(compoundClasses) {
|
|
|
726
774
|
const cleanedStyles = []
|
|
727
775
|
const paddingObject = []
|
|
728
776
|
const backgroundGradientObject = []
|
|
777
|
+
const backgroundGradientDirection = []
|
|
729
778
|
|
|
730
779
|
_.each(compoundClasses, value => {
|
|
731
780
|
if (compoundClasses.length > 1) {
|
|
732
781
|
if (value.includes('font:')) {
|
|
733
782
|
fontObject.push(value.replace('font: ', '').replace(/{(.*)}/, '$1').trim())
|
|
734
|
-
} else if (value.includes('backgroundGradient:
|
|
735
|
-
|
|
783
|
+
} else if (value.includes('backgroundGradient:')) {
|
|
784
|
+
// Split into 2 buckets: colors (from-X/to-X) vs direction (bg-gradient-to-X with type/startPoint/endPoint).
|
|
785
|
+
// Both share the same property name `backgroundGradient` so they MUST be merged into a single object,
|
|
786
|
+
// otherwise the later one overwrites the earlier one in the emitted JS object literal.
|
|
787
|
+
const inner = value.replace('backgroundGradient: ', '').replace(/{(.*)}/, '$1').trim()
|
|
788
|
+
if (inner.startsWith('colors')) {
|
|
789
|
+
backgroundGradientObject.push(inner)
|
|
790
|
+
} else {
|
|
791
|
+
backgroundGradientDirection.push(inner)
|
|
792
|
+
}
|
|
736
793
|
} else if (value.includes('padding:')) {
|
|
737
794
|
paddingObject.push(value.replace('padding: ', '').replace(/{(.*)}/, '$1').trim())
|
|
738
795
|
} else {
|
|
@@ -763,13 +820,28 @@ export function fixDuplicateKeys(compoundClasses) {
|
|
|
763
820
|
cleanedStyles.push(`font: { ${fontObject.sort().join(', ')} }`)
|
|
764
821
|
}
|
|
765
822
|
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
823
|
+
// Merge gradient direction (bg-gradient-to-X) and gradient colors (from-X/to-X)
|
|
824
|
+
// into a single backgroundGradient object. They share the same property name,
|
|
825
|
+
// so emitting them as separate entries causes the later one to overwrite the earlier.
|
|
826
|
+
if (backgroundGradientDirection.length || backgroundGradientObject.length) {
|
|
827
|
+
let colorsPart = ''
|
|
828
|
+
if (backgroundGradientObject.length === 1) {
|
|
829
|
+
colorsPart = backgroundGradientObject[0]
|
|
830
|
+
} else if (backgroundGradientObject.length === 2) {
|
|
831
|
+
// from-X emits 2 colors (placeholder + actual), to-X emits 1.
|
|
832
|
+
// After sort() above, indices may swap depending on color name ordering,
|
|
833
|
+
// so identify by array length instead of position.
|
|
834
|
+
const colorsA = backgroundGradientObject[0].replace('colors: ', '').replace(/[[\]']+/g, '').trim().split(',').map(c => c.trim())
|
|
835
|
+
const colorsB = backgroundGradientObject[1].replace('colors: ', '').replace(/[[\]']+/g, '').trim().split(',').map(c => c.trim())
|
|
836
|
+
const fromEntry = colorsA.length === 2 ? colorsA : colorsB
|
|
837
|
+
const toEntry = colorsA.length === 1 ? colorsA : colorsB
|
|
838
|
+
colorsPart = `colors: [ '${toEntry[0]}', '${fromEntry[1]}' ]`
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
const parts = []
|
|
842
|
+
if (backgroundGradientDirection.length) parts.push(backgroundGradientDirection[0])
|
|
843
|
+
if (colorsPart) parts.push(colorsPart)
|
|
844
|
+
cleanedStyles.push(`backgroundGradient: { ${parts.join(', ')} }`)
|
|
773
845
|
}
|
|
774
846
|
|
|
775
847
|
// Missing properties to process
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PurgeTSS - Semantic Colors Helpers
|
|
3
|
+
*
|
|
4
|
+
* Auto-derivation of semantic color keys with applied alpha for opacity
|
|
5
|
+
* modifiers (e.g. `bg-surface/65`). Mutates `semantic.colors.json` idempotently
|
|
6
|
+
* by adding `<originalKey>_<alphaPercent>` entries with `{ color, alpha }` for
|
|
7
|
+
* `light` and `dark`.
|
|
8
|
+
*
|
|
9
|
+
* Lifecycle:
|
|
10
|
+
* - Names of semantic colors are tracked via `registerSemanticName()` while
|
|
11
|
+
* the utilities builder expands `theme.extend.colors`.
|
|
12
|
+
* - Hooks in `tailwind-purger` and `compileApplyDirectives` call
|
|
13
|
+
* `deriveAlphaKey()` before falling back to warn/throw on missing hex.
|
|
14
|
+
* - CLI commands flush the cached JSON to disk via `flushSemanticColors()`
|
|
15
|
+
* at the end of their run.
|
|
16
|
+
*
|
|
17
|
+
* @fileoverview Semantic color auto-derivation for opacity modifiers
|
|
18
|
+
* @since 7.9.0
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import fs from 'fs'
|
|
22
|
+
import { getSemanticColorsPath } from '../cli/utils/project-detection.js'
|
|
23
|
+
|
|
24
|
+
const _semanticNames = new Set()
|
|
25
|
+
let _cache = null
|
|
26
|
+
let _dirty = false
|
|
27
|
+
let _lastMtime = 0
|
|
28
|
+
let _lastPath = null
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Reset all in-memory state. Intended for tests; not part of the public API.
|
|
32
|
+
*/
|
|
33
|
+
export function _resetSemanticHelpersState() {
|
|
34
|
+
_semanticNames.clear()
|
|
35
|
+
_cache = null
|
|
36
|
+
_dirty = false
|
|
37
|
+
_lastMtime = 0
|
|
38
|
+
_lastPath = null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Track a name that resolves to a semantic color (string, non-hex). Called by
|
|
43
|
+
* the utilities builder while expanding `theme.extend.colors`.
|
|
44
|
+
*
|
|
45
|
+
* @param {string} name
|
|
46
|
+
*/
|
|
47
|
+
export function registerSemanticName(name) {
|
|
48
|
+
if (typeof name === 'string' && name.length > 0) _semanticNames.add(name)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @param {string} name
|
|
53
|
+
* @returns {boolean}
|
|
54
|
+
*/
|
|
55
|
+
export function isSemanticColorName(name) {
|
|
56
|
+
return _semanticNames.has(name)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Load `semantic.colors.json` for the current project. Cached by mtime so
|
|
61
|
+
* repeated calls in the same build are cheap. The cache is invalidated when
|
|
62
|
+
* the file mtime changes on disk (relevant for watch-mode cycles where the
|
|
63
|
+
* JSON gets rewritten between runs).
|
|
64
|
+
*
|
|
65
|
+
* @returns {Object} parsed JSON, or `{}` when the file doesn't exist
|
|
66
|
+
*/
|
|
67
|
+
export function loadSemanticColors() {
|
|
68
|
+
const p = getSemanticColorsPath()
|
|
69
|
+
if (!fs.existsSync(p)) {
|
|
70
|
+
_cache = {}
|
|
71
|
+
_lastMtime = 0
|
|
72
|
+
_lastPath = p
|
|
73
|
+
return _cache
|
|
74
|
+
}
|
|
75
|
+
const mtime = fs.statSync(p).mtimeMs
|
|
76
|
+
if (_cache && _lastPath === p && mtime === _lastMtime) return _cache
|
|
77
|
+
_cache = JSON.parse(fs.readFileSync(p, 'utf8'))
|
|
78
|
+
_lastMtime = mtime
|
|
79
|
+
_lastPath = p
|
|
80
|
+
return _cache
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Derive a semantic key with the requested alpha applied. Idempotent: if the
|
|
85
|
+
* derived key already exists with matching values it's reused; if it exists
|
|
86
|
+
* with conflicting values an Error is thrown so manual edits aren't silently
|
|
87
|
+
* overwritten.
|
|
88
|
+
*
|
|
89
|
+
* @param {string} baseKey - The original semantic key (e.g. `surfaceColor`).
|
|
90
|
+
* @param {number|string} alphaPercent - Integer 0..100 (e.g. 65).
|
|
91
|
+
* @returns {string|null} The derived key, or `null` if `baseKey` isn't in the JSON.
|
|
92
|
+
*/
|
|
93
|
+
export function deriveAlphaKey(baseKey, alphaPercent) {
|
|
94
|
+
const semantic = loadSemanticColors()
|
|
95
|
+
const base = semantic[baseKey]
|
|
96
|
+
if (!base) return null
|
|
97
|
+
|
|
98
|
+
const alphaStr = String(alphaPercent)
|
|
99
|
+
const newKey = `${baseKey}_${alphaStr}`
|
|
100
|
+
|
|
101
|
+
const lightHex = typeof base.light === 'string' ? base.light : base.light?.color
|
|
102
|
+
const darkHex = typeof base.dark === 'string' ? base.dark : base.dark?.color
|
|
103
|
+
|
|
104
|
+
if (semantic[newKey]) {
|
|
105
|
+
const existing = semantic[newKey]
|
|
106
|
+
const existingLightHex = typeof existing.light === 'string' ? existing.light : existing.light?.color
|
|
107
|
+
const existingDarkHex = typeof existing.dark === 'string' ? existing.dark : existing.dark?.color
|
|
108
|
+
const existingLightAlpha = typeof existing.light === 'string' ? null : existing.light?.alpha
|
|
109
|
+
const existingDarkAlpha = typeof existing.dark === 'string' ? null : existing.dark?.alpha
|
|
110
|
+
|
|
111
|
+
if (
|
|
112
|
+
existingLightHex !== lightHex ||
|
|
113
|
+
existingDarkHex !== darkHex ||
|
|
114
|
+
existingLightAlpha !== alphaStr ||
|
|
115
|
+
existingDarkAlpha !== alphaStr
|
|
116
|
+
) {
|
|
117
|
+
throw new Error(
|
|
118
|
+
`Conflict: "${newKey}" already exists in semantic.colors.json with different values. Manual edit detected — resolve before continuing.`
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
return newKey
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
semantic[newKey] = {
|
|
125
|
+
light: { color: lightHex, alpha: alphaStr },
|
|
126
|
+
dark: { color: darkHex, alpha: alphaStr }
|
|
127
|
+
}
|
|
128
|
+
_dirty = true
|
|
129
|
+
return newKey
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Persist any pending derivations back to `semantic.colors.json`. No-op when
|
|
134
|
+
* nothing changed. Writes a trailing newline for clean git diffs.
|
|
135
|
+
*/
|
|
136
|
+
export function flushSemanticColors() {
|
|
137
|
+
if (!_dirty) return
|
|
138
|
+
const p = getSemanticColorsPath()
|
|
139
|
+
fs.writeFileSync(p, JSON.stringify(_cache, null, 2) + '\n')
|
|
140
|
+
_dirty = false
|
|
141
|
+
_lastMtime = fs.statSync(p).mtimeMs
|
|
142
|
+
_lastPath = p
|
|
143
|
+
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* PurgeTSS v7.1 - Tailwind Builder (Development)
|
|
3
|
-
*
|
|
4
|
-
* Builds Tailwind CSS files for development/distribution using auto-generation.
|
|
5
|
-
* COPIED from lib/build-tailwind.js - NO CHANGES to logic.
|
|
6
|
-
*
|
|
7
|
-
* Generates: ./dist/utilities.tss
|
|
8
|
-
*
|
|
9
|
-
* @since 7.1.0 (refactored from lib/)
|
|
10
|
-
* @author César Estrada
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { autoBuildUtilitiesTSS } from '../../../experimental/completions2.js'
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Main builder function
|
|
17
|
-
* COPIED exactly from original constructor() function
|
|
18
|
-
*/
|
|
19
|
-
export function buildTailwind() {
|
|
20
|
-
autoBuildUtilitiesTSS()
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// Execute if run directly
|
|
24
|
-
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
25
|
-
buildTailwind()
|
|
26
|
-
}
|