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.
@@ -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 defaultHexValue = (twClassesArray[opacityIndex].includes('from')) ? twClassesArray[opacityIndex].match(/#[0-9a-f]{6}/g)[1] : twClassesArray[opacityIndex].match(/#[0-9a-f]{6}/i)[0]
534
- const classWithoutDecimalOpacity = `${twClassesArray[opacityIndex].replace(new RegExp(defaultHexValue.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), `#${opacityValue.transparency}${defaultHexValue.substring(1)}`)}`
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
- twClassesArray[indexOfModifier] = _.replace(twClassesArray[indexOfModifier], /{_applyProperties_}/, fixDuplicateKeys(compoundClasses).join(', '))
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 splitedContent = (arbitraryValue.startsWith('-')) ? arbitraryValue.substring(1).split('-') : arbitraryValue.split('-')
640
+ const stripped = sign ? arbitraryValue.substring(1) : arbitraryValue
597
641
 
598
- if (splitedContent.length === 1) {
599
- return ''
600
- } else if (splitedContent.length === 2) {
601
- let rule = splitedContent.slice(0, 1).join('-')
602
- let value = splitedContent[1].match(/(?<=\().*(?=\))/).pop()
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 (splitedContent.length === 3) {
634
- const rule = splitedContent.slice(0, 2).join('-')
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 (splitedContent[0] === 'rounded') {
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 (splitedContent.length === 4) {
655
- const rule = splitedContent.slice(0, 3).join('-')
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 (splitedContent.length === 5) {
669
- const rule = splitedContent.slice(0, 4).join('-')
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: { colors')) {
735
- backgroundGradientObject.push(value.replace('backgroundGradient: ', '').replace(/{(.*)}/, '$1').trim())
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
- if (backgroundGradientObject.length === 1) {
767
- cleanedStyles.push(`backgroundGradient: { ${backgroundGradientObject} }`)
768
- } else if (backgroundGradientObject.length === 2) {
769
- const toColor = backgroundGradientObject[1].replace('colors: ', '').replace(/[[\]']+/g, '').trim().split(',')
770
- const fromToColors = backgroundGradientObject[0].replace('colors: ', '').replace(/[[\]']+/g, '').trim().split(',')
771
- fromToColors[0] = toColor[0]
772
- cleanedStyles.push(`backgroundGradient: { colors: [ '${fromToColors[0]}', '${fromToColors[1].trim()}' ] }`)
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
- }