purgetss 7.7.1 → 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 +8 -0
- package/bin/purgetss +10 -0
- package/dist/purgetss.ui.js +1 -1
- 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/{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
|
@@ -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
|
-
}
|