purgetss 7.7.1 → 7.11.1
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 +28 -0
- package/bin/purgetss +23 -0
- package/dist/purgetss.ui.js +1 -1
- package/lib/templates/create/index.xml +1 -1
- package/lib/templates/purgetss.config.js.cjs +3 -1
- package/package.json +2 -2
- package/src/cli/commands/build.js +9 -4
- package/src/cli/commands/images.js +49 -2
- package/src/cli/commands/purge.js +31 -4
- package/src/cli/commands/shades.js +2 -2
- package/src/cli/utils/cli-helpers.js +15 -5
- package/src/cli/utils/unsupported-class-reporter.js +209 -0
- package/src/core/analyzers/class-extractor.js +54 -0
- package/src/core/analyzers/controller-svg-refs.js +154 -0
- package/src/core/branding/brand-config.js +7 -0
- package/src/core/branding/ensure-brand-section.js +4 -3
- package/src/core/branding/gen-feature-graphic.js +57 -0
- package/src/core/branding/index.js +28 -4
- package/src/core/branding/post-gen-notes.js +2 -2
- package/{experimental/completions2.js → src/core/builders/auto-utilities-builder.js} +74 -40
- package/src/core/builders/tailwind-builder.js +2 -2
- package/src/core/builders/tailwind-helpers.js +0 -444
- package/src/core/images/ensure-images-section.js +6 -4
- package/src/core/images/gen-scales.js +96 -13
- package/src/core/images/index.js +121 -9
- package/src/core/purger/icon-purger.js +7 -3
- package/src/core/purger/tailwind-purger.js +43 -5
- package/src/core/svg/cache.js +96 -0
- package/src/core/svg/derive-dimensions.js +120 -0
- package/src/core/svg/index.js +215 -0
- package/src/core/svg/resolve-classes.js +46 -0
- package/src/core/svg/sync-images.js +278 -0
- package/src/core/svg/tss-reader.js +134 -0
- package/src/dev/builders/tailwind-builder.js +3 -11
- package/src/shared/config-manager.js +72 -3
- package/src/shared/error-reporter.js +117 -0
- package/src/shared/helpers/colors.js +57 -13
- package/src/shared/helpers/core.js +0 -19
- package/src/shared/helpers/utils.js +146 -36
- package/src/shared/logger.js +12 -0
- package/src/shared/semantic-helpers.js +143 -0
- package/src/shared/validation/config-validator.js +167 -0
|
@@ -1,26 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* PurgeTSS
|
|
3
|
-
*
|
|
4
|
-
* Builds Tailwind CSS files for development/distribution using auto-generation.
|
|
5
|
-
* COPIED from lib/build-tailwind.js - NO CHANGES to logic.
|
|
2
|
+
* PurgeTSS - Utilities Builder (Development entry point)
|
|
6
3
|
*
|
|
4
|
+
* Thin CLI wrapper invoked by the `build:tailwind` npm script.
|
|
7
5
|
* Generates: ./dist/utilities.tss
|
|
8
6
|
*
|
|
9
|
-
* @since 7.1.0 (refactored from lib/)
|
|
10
7
|
* @author César Estrada
|
|
11
8
|
*/
|
|
12
9
|
|
|
13
|
-
import { autoBuildUtilitiesTSS } from '
|
|
10
|
+
import { autoBuildUtilitiesTSS } from '../../core/builders/auto-utilities-builder.js'
|
|
14
11
|
|
|
15
|
-
/**
|
|
16
|
-
* Main builder function
|
|
17
|
-
* COPIED exactly from original constructor() function
|
|
18
|
-
*/
|
|
19
12
|
export function buildTailwind() {
|
|
20
13
|
autoBuildUtilitiesTSS()
|
|
21
14
|
}
|
|
22
15
|
|
|
23
|
-
// Execute if run directly
|
|
24
16
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
25
17
|
buildTailwind()
|
|
26
18
|
}
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
} from './constants.js'
|
|
24
24
|
import { logger } from './logger.js'
|
|
25
25
|
import { makeSureFolderExists } from './utils.js'
|
|
26
|
+
import { validateConfig } from './validation/config-validator.js'
|
|
26
27
|
|
|
27
28
|
// Create require for ESM compatibility
|
|
28
29
|
const require = createRequire(import.meta.url)
|
|
@@ -105,6 +106,71 @@ export function migrateConfigIfNeeded() {
|
|
|
105
106
|
}
|
|
106
107
|
}
|
|
107
108
|
|
|
109
|
+
// Tracks configs already warned about in this process so the deprecation
|
|
110
|
+
// notice prints once per session even if getConfigFile() is called many times.
|
|
111
|
+
const _warnedLegacyBrand = new Set()
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Migrate the flat pre-7cb5890 `brand:` schema (padding as number, iosPadding,
|
|
115
|
+
* bgColor, darkBgColor, top-level notification/splash) into the grouped
|
|
116
|
+
* schema downstream code expects. Mutates in place; if both legacy and new
|
|
117
|
+
* keys coexist, the new key wins. Emits ONE warning per config path per session.
|
|
118
|
+
*/
|
|
119
|
+
function normalizeLegacyBrand(configFile, sourcePath) {
|
|
120
|
+
const brand = configFile.brand
|
|
121
|
+
if (!brand || typeof brand !== 'object') return
|
|
122
|
+
|
|
123
|
+
const hits = []
|
|
124
|
+
|
|
125
|
+
if (brand.padding != null && typeof brand.padding !== 'object') {
|
|
126
|
+
const value = brand.padding
|
|
127
|
+
brand.padding = { androidLegacy: value, androidAdaptive: value }
|
|
128
|
+
hits.push(`brand.padding: ${JSON.stringify(value)} → brand.padding.androidLegacy + brand.padding.androidAdaptive`)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if ('iosPadding' in brand) {
|
|
132
|
+
brand.padding = (brand.padding && typeof brand.padding === 'object') ? brand.padding : {}
|
|
133
|
+
brand.padding.ios = brand.padding.ios ?? brand.iosPadding
|
|
134
|
+
hits.push('brand.iosPadding → brand.padding.ios')
|
|
135
|
+
delete brand.iosPadding
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if ('bgColor' in brand) {
|
|
139
|
+
brand.colors = brand.colors ?? {}
|
|
140
|
+
brand.colors.background = brand.colors.background ?? brand.bgColor
|
|
141
|
+
hits.push('brand.bgColor → brand.colors.background')
|
|
142
|
+
delete brand.bgColor
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if ('darkBgColor' in brand) {
|
|
146
|
+
brand.ios = brand.ios ?? {}
|
|
147
|
+
brand.ios.darkBackground = brand.ios.darkBackground ?? brand.darkBgColor
|
|
148
|
+
hits.push('brand.darkBgColor → brand.ios.darkBackground')
|
|
149
|
+
delete brand.darkBgColor
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if ('notification' in brand) {
|
|
153
|
+
brand.android = brand.android ?? {}
|
|
154
|
+
brand.android.notification = brand.android.notification ?? brand.notification
|
|
155
|
+
hits.push('brand.notification → brand.android.notification')
|
|
156
|
+
delete brand.notification
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if ('splash' in brand) {
|
|
160
|
+
brand.android = brand.android ?? {}
|
|
161
|
+
brand.android.splash = brand.android.splash ?? brand.splash
|
|
162
|
+
hits.push('brand.splash → brand.android.splash')
|
|
163
|
+
delete brand.splash
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (hits.length > 0 && !_warnedLegacyBrand.has(sourcePath)) {
|
|
167
|
+
_warnedLegacyBrand.add(sourcePath)
|
|
168
|
+
logger.warn('Legacy brand: schema detected in purgetss/config.cjs — auto-migrated in memory:')
|
|
169
|
+
for (const hit of hits) logger.item(` • ${hit}`)
|
|
170
|
+
logger.item(' Update purgetss/config.cjs to the new grouped schema to silence this warning.')
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
108
174
|
/**
|
|
109
175
|
* Get configuration file with fallback to default template
|
|
110
176
|
* Maintains exact same logic as original getConfigFile()
|
|
@@ -113,9 +179,11 @@ export function migrateConfigIfNeeded() {
|
|
|
113
179
|
*/
|
|
114
180
|
export function getConfigFile() {
|
|
115
181
|
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
182
|
+
const sourcePath = fs.existsSync(projectsConfigJS) ? projectsConfigJS : srcConfigFile
|
|
183
|
+
const configFile = require(sourcePath)
|
|
184
|
+
|
|
185
|
+
validateConfig(configFile, sourcePath)
|
|
186
|
+
normalizeLegacyBrand(configFile, sourcePath)
|
|
119
187
|
|
|
120
188
|
// Apply default values following template structure
|
|
121
189
|
configFile.purge = configFile.purge ?? {}
|
|
@@ -133,6 +201,7 @@ export function getConfigFile() {
|
|
|
133
201
|
configFile.brand.padding.ios = parsePadding(configFile.brand.padding.ios ?? 4, 'brand.padding.ios')
|
|
134
202
|
configFile.brand.padding.androidLegacy = parsePadding(configFile.brand.padding.androidLegacy ?? 10, 'brand.padding.androidLegacy')
|
|
135
203
|
configFile.brand.padding.androidAdaptive = parsePadding(configFile.brand.padding.androidAdaptive ?? 19, 'brand.padding.androidAdaptive')
|
|
204
|
+
configFile.brand.padding.featureGraphic = parsePadding(configFile.brand.padding.featureGraphic ?? 12, 'brand.padding.featureGraphic')
|
|
136
205
|
configFile.brand.android = configFile.brand.android ?? {}
|
|
137
206
|
configFile.brand.android.notification = configFile.brand.android.notification ?? false
|
|
138
207
|
configFile.brand.android.splash = configFile.brand.android.splash ?? false
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized error reporter for syntax errors detected by PurgeTSS.
|
|
3
|
+
*
|
|
4
|
+
* Visual format matches the patterns already used by:
|
|
5
|
+
* - XML Syntax Error (src/cli/commands/purge.js, "compact" variant)
|
|
6
|
+
* - Class Syntax Error (src/cli/utils/unsupported-class-reporter.js)
|
|
7
|
+
* - XML Syntax Error with Context (src/cli/commands/purge.js, "context" variant)
|
|
8
|
+
*
|
|
9
|
+
* Those call sites are NOT migrated yet — they keep their own inline formatters
|
|
10
|
+
* to avoid any chance of visual regression. New validators (config-validator)
|
|
11
|
+
* use this module so the look is consistent going forward.
|
|
12
|
+
*
|
|
13
|
+
* Two output paths:
|
|
14
|
+
* - formatSyntaxError(opts) → { header, lines } suitable for logger.block(...)
|
|
15
|
+
* - throwSyntaxError(opts) → throws an Error whose .message is the full
|
|
16
|
+
* pre-rendered string (for call sites that
|
|
17
|
+
* propagate errors via catch handlers).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import chalk from 'chalk'
|
|
21
|
+
import path from 'path'
|
|
22
|
+
import { logger } from './logger.js'
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Build the displayable parts of a syntax error.
|
|
26
|
+
*
|
|
27
|
+
* @param {Object} opts
|
|
28
|
+
* @param {string} opts.type - Short name shown in the header, e.g. 'Config', 'XML', 'Class'.
|
|
29
|
+
* @param {string} [opts.file] - File path (will be made relative to cwd).
|
|
30
|
+
* @param {string|number} [opts.path] - Dotted JSON path (optional, for config errors).
|
|
31
|
+
* @param {number} [opts.line] - Line number where the error occurred.
|
|
32
|
+
* @param {string} [opts.content] - One-line snippet of the offending line.
|
|
33
|
+
* @param {string[]} [opts.contextLines] - Full file lines (1-based; pass src.split('\n') OK with 0-based,
|
|
34
|
+
* but `line` must point to a 1-based number; we slice ±2).
|
|
35
|
+
* @param {string} opts.issue - Description of what is wrong.
|
|
36
|
+
* @param {string} opts.fix - Suggested correction.
|
|
37
|
+
* @returns {{ header: string, lines: string[] }}
|
|
38
|
+
*/
|
|
39
|
+
export function formatSyntaxError(opts) {
|
|
40
|
+
const {
|
|
41
|
+
type,
|
|
42
|
+
file,
|
|
43
|
+
path: jsonPath,
|
|
44
|
+
line,
|
|
45
|
+
content,
|
|
46
|
+
contextLines,
|
|
47
|
+
issue,
|
|
48
|
+
fix
|
|
49
|
+
} = opts
|
|
50
|
+
|
|
51
|
+
const lines = []
|
|
52
|
+
|
|
53
|
+
if (file) {
|
|
54
|
+
const relative = path.relative(process.cwd(), file) || file
|
|
55
|
+
lines.push(`File: ${chalk.yellow(`"${relative}"`)}`)
|
|
56
|
+
}
|
|
57
|
+
if (jsonPath) {
|
|
58
|
+
lines.push(`Path: ${chalk.yellow(jsonPath)}`)
|
|
59
|
+
}
|
|
60
|
+
if (line != null) {
|
|
61
|
+
lines.push(`Line: ${chalk.yellow(line)}`)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Context block: ± 2 lines around the offending line.
|
|
65
|
+
if (Array.isArray(contextLines) && line) {
|
|
66
|
+
const total = contextLines.length
|
|
67
|
+
const startLine = Math.max(1, line - 2)
|
|
68
|
+
const endLine = Math.min(total, line + 2)
|
|
69
|
+
|
|
70
|
+
lines.push('')
|
|
71
|
+
lines.push(chalk.gray('Context:'))
|
|
72
|
+
for (let i = startLine; i <= endLine; i++) {
|
|
73
|
+
const isTarget = i === line
|
|
74
|
+
const prefix = isTarget ? chalk.red('>>>') : chalk.gray(' ')
|
|
75
|
+
const text = contextLines[i - 1] || ''
|
|
76
|
+
lines.push(`${prefix} ${chalk.gray(String(i).padStart(3, ' '))}: ${text}`)
|
|
77
|
+
}
|
|
78
|
+
} else if (content) {
|
|
79
|
+
lines.push(`Content: ${chalk.yellow(`"${content}"`)}`)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
lines.push('')
|
|
83
|
+
lines.push(chalk.red(`Issue: ${issue}`))
|
|
84
|
+
lines.push(`${chalk.green('Fix:')} ${fix}`)
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
header: chalk.red(`${type} Syntax Error`),
|
|
88
|
+
lines
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Log the syntax error directly via logger.block.
|
|
94
|
+
* Use when you want the error rendered now and execution to continue
|
|
95
|
+
* (or stop via a sentinel afterward).
|
|
96
|
+
*/
|
|
97
|
+
export function logSyntaxError(opts) {
|
|
98
|
+
const { header, lines } = formatSyntaxError(opts)
|
|
99
|
+
logger.block(header, ...lines)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Throw an Error whose .message is the fully rendered report.
|
|
104
|
+
* Use when the error must bubble up through a catch handler that prints
|
|
105
|
+
* `error.message` (e.g. the top-level CLI catch in bin/purgetss).
|
|
106
|
+
*
|
|
107
|
+
* The thrown Error includes `isSyntaxError: true` so callers can distinguish
|
|
108
|
+
* presentation-ready errors from generic runtime failures.
|
|
109
|
+
*/
|
|
110
|
+
export function throwSyntaxError(opts) {
|
|
111
|
+
const { header, lines } = formatSyntaxError(opts)
|
|
112
|
+
const text = `\n::PurgeTSS:: ${header}\n` + lines.map(l => ' ' + l).join('\n') + '\n'
|
|
113
|
+
const err = new Error(text)
|
|
114
|
+
err.isSyntaxError = true
|
|
115
|
+
err.syntaxErrorType = opts.type
|
|
116
|
+
throw err
|
|
117
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import _ from 'lodash'
|
|
2
|
-
import { processProperties, processComments, parseValue, setModifier2, removeLastDash, addTransparencyToValue } from './utils.js'
|
|
2
|
+
import { processProperties, processComments, parseValue, setModifier2, removeLastDash, addTransparencyToValue, defaultModifier, camelCaseToDash } from './utils.js'
|
|
3
3
|
/**
|
|
4
4
|
* Active tint color for tabs
|
|
5
5
|
* @param {Object} modifiersAndValues - Modifier and value pairs
|
|
@@ -646,9 +646,20 @@ export function backgroundGradient(modifiersAndValues) {
|
|
|
646
646
|
_.each(objectPosition, (properties, rule) => {
|
|
647
647
|
_.each(modifiersAndValues, (value, modifier) => {
|
|
648
648
|
if (typeof value === 'object') {
|
|
649
|
-
|
|
650
|
-
convertedStyles += `'.${removeLastDash(`${rule}-${setModifier2(modifier, rule)}${setModifier2(_modifier)}`)}': ` + _.replace(_.replace(properties, /{transparentValue}/g, `${addTransparencyToValue(parseValue(
|
|
651
|
-
}
|
|
649
|
+
const emitLeaf = (leafValue, _modifier, suffix) => {
|
|
650
|
+
convertedStyles += `'.${removeLastDash(`${rule}-${setModifier2(modifier, rule)}${setModifier2(_modifier)}${suffix}`)}': ` + _.replace(_.replace(properties, /{transparentValue}/g, `${addTransparencyToValue(parseValue(leafValue))}`), /{value}/g, parseValue(leafValue)) + '\n'
|
|
651
|
+
}
|
|
652
|
+
const walk = (val, _modifier, suffix) => {
|
|
653
|
+
if (val && typeof val === 'object') {
|
|
654
|
+
_.each(val, (childVal, childKey) => {
|
|
655
|
+
const newSuffix = defaultModifier(childKey) ? suffix : `${suffix}-${camelCaseToDash(String(childKey))}`
|
|
656
|
+
walk(childVal, _modifier, newSuffix)
|
|
657
|
+
})
|
|
658
|
+
} else {
|
|
659
|
+
emitLeaf(val, _modifier, suffix)
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
_.each(value, (_value, _modifier) => walk(_value, _modifier, ''))
|
|
652
663
|
} else {
|
|
653
664
|
convertedStyles += `'.${setModifier2(rule, modifier)}${setModifier2(modifier)}': ` + _.replace(_.replace(properties, /{value}/g, parseValue(value)), /{transparentValue}/g, `${addTransparencyToValue(parseValue(value))}`) + '\n'
|
|
654
665
|
}
|
|
@@ -665,9 +676,20 @@ export function backgroundGradient(modifiersAndValues) {
|
|
|
665
676
|
_.each(objectPosition, (properties, rule) => {
|
|
666
677
|
_.each(modifiersAndValues, (value, modifier) => {
|
|
667
678
|
if (typeof value === 'object') {
|
|
668
|
-
|
|
669
|
-
convertedStyles += `'.${removeLastDash(`${rule}-${setModifier2(modifier, rule)}${setModifier2(_modifier)}`)}': ` + _.replace(properties, /{value}/g, parseValue(
|
|
670
|
-
}
|
|
679
|
+
const emitLeaf = (leafValue, _modifier, suffix) => {
|
|
680
|
+
convertedStyles += `'.${removeLastDash(`${rule}-${setModifier2(modifier, rule)}${setModifier2(_modifier)}${suffix}`)}': ` + _.replace(properties, /{value}/g, parseValue(leafValue)) + '\n'
|
|
681
|
+
}
|
|
682
|
+
const walk = (val, _modifier, suffix) => {
|
|
683
|
+
if (val && typeof val === 'object') {
|
|
684
|
+
_.each(val, (childVal, childKey) => {
|
|
685
|
+
const newSuffix = defaultModifier(childKey) ? suffix : `${suffix}-${camelCaseToDash(String(childKey))}`
|
|
686
|
+
walk(childVal, _modifier, newSuffix)
|
|
687
|
+
})
|
|
688
|
+
} else {
|
|
689
|
+
emitLeaf(val, _modifier, suffix)
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
_.each(value, (_value, _modifier) => walk(_value, _modifier, ''))
|
|
671
693
|
} else {
|
|
672
694
|
convertedStyles += `'.${setModifier2(rule, modifier)}${setModifier2(modifier)}': ` + _.replace(properties, /{value}/g, parseValue(value)) + '\n'
|
|
673
695
|
}
|
|
@@ -688,9 +710,20 @@ export function backgroundSelectedGradient(modifiersAndValues) {
|
|
|
688
710
|
_.each(objectPosition, (properties, rule) => {
|
|
689
711
|
_.each(modifiersAndValues, (value, modifier) => {
|
|
690
712
|
if (typeof value === 'object') {
|
|
691
|
-
|
|
692
|
-
convertedStyles += `'.${removeLastDash(`${rule}-${setModifier2(modifier, rule)}${setModifier2(_modifier)}`)}': ` + _.replace(_.replace(properties, /{transparentValue}/g, `${addTransparencyToValue(parseValue(
|
|
693
|
-
}
|
|
713
|
+
const emitLeaf = (leafValue, _modifier, suffix) => {
|
|
714
|
+
convertedStyles += `'.${removeLastDash(`${rule}-${setModifier2(modifier, rule)}${setModifier2(_modifier)}${suffix}`)}': ` + _.replace(_.replace(properties, /{transparentValue}/g, `${addTransparencyToValue(parseValue(leafValue))}`), /{value}/g, parseValue(leafValue)) + '\n'
|
|
715
|
+
}
|
|
716
|
+
const walk = (val, _modifier, suffix) => {
|
|
717
|
+
if (val && typeof val === 'object') {
|
|
718
|
+
_.each(val, (childVal, childKey) => {
|
|
719
|
+
const newSuffix = defaultModifier(childKey) ? suffix : `${suffix}-${camelCaseToDash(String(childKey))}`
|
|
720
|
+
walk(childVal, _modifier, newSuffix)
|
|
721
|
+
})
|
|
722
|
+
} else {
|
|
723
|
+
emitLeaf(val, _modifier, suffix)
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
_.each(value, (_value, _modifier) => walk(_value, _modifier, ''))
|
|
694
727
|
} else {
|
|
695
728
|
convertedStyles += `'.${setModifier2(rule, modifier)}${setModifier2(modifier)}': ` + _.replace(_.replace(properties, /{value}/g, parseValue(value)), /{transparentValue}/g, `${addTransparencyToValue(parseValue(value))}`) + '\n'
|
|
696
729
|
}
|
|
@@ -707,9 +740,20 @@ export function backgroundSelectedGradient(modifiersAndValues) {
|
|
|
707
740
|
_.each(objectPosition, (properties, rule) => {
|
|
708
741
|
_.each(modifiersAndValues, (value, modifier) => {
|
|
709
742
|
if (typeof value === 'object') {
|
|
710
|
-
|
|
711
|
-
convertedStyles += `'.${removeLastDash(`${rule}-${setModifier2(modifier, rule)}${setModifier2(_modifier)}`)}': ` + _.replace(properties, /{value}/g, parseValue(
|
|
712
|
-
}
|
|
743
|
+
const emitLeaf = (leafValue, _modifier, suffix) => {
|
|
744
|
+
convertedStyles += `'.${removeLastDash(`${rule}-${setModifier2(modifier, rule)}${setModifier2(_modifier)}${suffix}`)}': ` + _.replace(properties, /{value}/g, parseValue(leafValue)) + '\n'
|
|
745
|
+
}
|
|
746
|
+
const walk = (val, _modifier, suffix) => {
|
|
747
|
+
if (val && typeof val === 'object') {
|
|
748
|
+
_.each(val, (childVal, childKey) => {
|
|
749
|
+
const newSuffix = defaultModifier(childKey) ? suffix : `${suffix}-${camelCaseToDash(String(childKey))}`
|
|
750
|
+
walk(childVal, _modifier, newSuffix)
|
|
751
|
+
})
|
|
752
|
+
} else {
|
|
753
|
+
emitLeaf(val, _modifier, suffix)
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
_.each(value, (_value, _modifier) => walk(_value, _modifier, ''))
|
|
713
757
|
} else {
|
|
714
758
|
convertedStyles += `'.${setModifier2(rule, modifier)}${setModifier2(modifier)}': ` + _.replace(properties, /{value}/g, parseValue(value)) + '\n'
|
|
715
759
|
}
|
|
@@ -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,13 @@
|
|
|
1
1
|
import fs from 'fs'
|
|
2
2
|
import _ from 'lodash'
|
|
3
|
+
import { deriveAlphaKey } from '../semantic-helpers.js'
|
|
4
|
+
import {
|
|
5
|
+
projectsFA_TSS_File,
|
|
6
|
+
srcFontAwesomeTSSFile,
|
|
7
|
+
srcMaterialIconsTSSFile,
|
|
8
|
+
srcMaterialSymbolsTSSFile,
|
|
9
|
+
srcFramework7FontTSSFile
|
|
10
|
+
} from '../constants.js'
|
|
3
11
|
|
|
4
12
|
// Internal variables and constants
|
|
5
13
|
const _applyClasses = {}
|
|
@@ -29,14 +37,25 @@ export function processProperties(info, selectorAndDeclarationBlock, selectorsAn
|
|
|
29
37
|
_.each(rulesAndValuesPair, (value, rule) => {
|
|
30
38
|
if (debug) console.log('rule:', rule, 'value:', value)
|
|
31
39
|
if (typeof value === 'object') {
|
|
32
|
-
|
|
33
|
-
if (debug) console.log('key:', key, '
|
|
34
|
-
let processedProperties = _.replace(declarationBlock, /{value}/g, parseValue(
|
|
40
|
+
const emitLeaf = (leafValue, key, suffix) => {
|
|
41
|
+
if (debug) console.log('key:', key, 'leafValue:', leafValue, 'suffix:', suffix)
|
|
42
|
+
let processedProperties = _.replace(declarationBlock, /{value}/g, parseValue(leafValue, minusSigns))
|
|
35
43
|
if (declarationBlock.includes('double')) {
|
|
36
|
-
processedProperties = _.replace(processedProperties, /{double}/g, parseValue(
|
|
44
|
+
processedProperties = _.replace(processedProperties, /{double}/g, parseValue(leafValue, minusSigns) * 2)
|
|
37
45
|
}
|
|
38
|
-
convertedStyles += defaultModifier(key) ? `'.${setModifier2(mainSelector, rule)}${setModifier2(rule)}${setModifier2(selector)}': ${processedProperties}\n` : `'.${setModifier2(mainSelector, rule)}${setModifier2(rule, key)}${setModifier2(key)}${setModifier2(selector)}': ${processedProperties}\n`
|
|
39
|
-
}
|
|
46
|
+
convertedStyles += defaultModifier(key) ? `'.${setModifier2(mainSelector, rule)}${setModifier2(rule)}${suffix}${setModifier2(selector)}': ${processedProperties}\n` : `'.${setModifier2(mainSelector, rule)}${setModifier2(rule, key)}${setModifier2(key)}${suffix}${setModifier2(selector)}': ${processedProperties}\n`
|
|
47
|
+
}
|
|
48
|
+
const walk = (val, key, suffix) => {
|
|
49
|
+
if (val && typeof val === 'object') {
|
|
50
|
+
_.each(val, (childVal, childKey) => {
|
|
51
|
+
const newSuffix = defaultModifier(childKey) ? suffix : `${suffix}-${camelCaseToDash(String(childKey))}`
|
|
52
|
+
walk(childVal, key, newSuffix)
|
|
53
|
+
})
|
|
54
|
+
} else {
|
|
55
|
+
emitLeaf(val, key, suffix)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
_.each(value, (_value, key) => walk(_value, key, ''))
|
|
40
59
|
} else {
|
|
41
60
|
let processedProperties = _.replace(declarationBlock, /{value}/g, parseValue(value, minusSigns))
|
|
42
61
|
if (declarationBlock.includes('double')) {
|
|
@@ -463,6 +482,18 @@ export function compileApplyDirectives(twClasses) {
|
|
|
463
482
|
const twClassesArray = twClasses.split(/\r?\n/)
|
|
464
483
|
const fontsClassesArray = (fs.existsSync(cwd + '/purgetss/styles/fonts.tss')) ? fs.readFileSync(cwd + '/purgetss/styles/fonts.tss', 'utf8').split(/\r?\n/) : null
|
|
465
484
|
|
|
485
|
+
// Default icon font sources (FontAwesome, Material Icons/Symbols, Framework7).
|
|
486
|
+
// Project-level fontawesome.tss (Pro/Beta) takes precedence over the bundled default,
|
|
487
|
+
// matching the precedence used by purgeFontAwesome().
|
|
488
|
+
const iconClassesArrays = [
|
|
489
|
+
fs.existsSync(projectsFA_TSS_File) ? projectsFA_TSS_File : srcFontAwesomeTSSFile,
|
|
490
|
+
srcMaterialIconsTSSFile,
|
|
491
|
+
srcMaterialSymbolsTSSFile,
|
|
492
|
+
srcFramework7FontTSSFile
|
|
493
|
+
]
|
|
494
|
+
.filter(p => fs.existsSync(p))
|
|
495
|
+
.map(p => fs.readFileSync(p, 'utf8').split(/\r?\n/))
|
|
496
|
+
|
|
466
497
|
_.each(_applyClasses, (values, className) => {
|
|
467
498
|
const indexOfModifier = findIndexOfClassName(`'${className}':`, twClassesArray)
|
|
468
499
|
|
|
@@ -518,6 +549,14 @@ export function compileApplyDirectives(twClasses) {
|
|
|
518
549
|
if (!foundClass && fontsClassesArray) {
|
|
519
550
|
foundClass = fontsClassesArray[findIndexOfClassName(genericClassName, fontsClassesArray)]
|
|
520
551
|
}
|
|
552
|
+
// Last resort: search default icon font sources (FontAwesome, Material Icons/Symbols, Framework7)
|
|
553
|
+
// so apply: directives can use fas, fa-*, mi-*, ms-*, f7-* without requiring fonts.tss
|
|
554
|
+
if (!foundClass) {
|
|
555
|
+
for (const arr of iconClassesArrays) {
|
|
556
|
+
const idx = findIndexOfClassName(genericClassName, arr)
|
|
557
|
+
if (idx !== -1) { foundClass = arr[idx]; break }
|
|
558
|
+
}
|
|
559
|
+
}
|
|
521
560
|
}
|
|
522
561
|
|
|
523
562
|
if (foundClass) compoundClasses.push(justProperties(foundClass))
|
|
@@ -530,15 +569,38 @@ export function compileApplyDirectives(twClasses) {
|
|
|
530
569
|
const opacityIndex = findIndexOfClassName(`'.${opacityValue.classNameWithTransparency}`, twClassesArray)
|
|
531
570
|
|
|
532
571
|
if (opacityIndex > -1) {
|
|
533
|
-
const
|
|
534
|
-
const
|
|
572
|
+
const targetLine = twClassesArray[opacityIndex]
|
|
573
|
+
const hexMatches = targetLine.match(/#[0-9a-f]{6}/gi)
|
|
574
|
+
|
|
575
|
+
if (!hexMatches || (targetLine.includes('from') && hexMatches.length < 2)) {
|
|
576
|
+
const derivedLine = tryDeriveSemanticOpacity(targetLine, opacityValue.decimalValue)
|
|
577
|
+
if (derivedLine) {
|
|
578
|
+
compoundClasses.push(justProperties(derivedLine))
|
|
579
|
+
return
|
|
580
|
+
}
|
|
581
|
+
throw new Error(
|
|
582
|
+
`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}".`
|
|
583
|
+
)
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const defaultHexValue = targetLine.includes('from') ? hexMatches[1] : hexMatches[0]
|
|
587
|
+
const classWithoutDecimalOpacity = `${targetLine.replace(new RegExp(defaultHexValue.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), `#${opacityValue.transparency}${defaultHexValue.substring(1)}`)}`
|
|
535
588
|
|
|
536
589
|
compoundClasses.push(justProperties(classWithoutDecimalOpacity))
|
|
537
590
|
}
|
|
538
591
|
})
|
|
539
592
|
}
|
|
540
593
|
|
|
541
|
-
|
|
594
|
+
let mergedProperties
|
|
595
|
+
try {
|
|
596
|
+
mergedProperties = fixDuplicateKeys(compoundClasses).join(', ')
|
|
597
|
+
} catch (mergeError) {
|
|
598
|
+
const classList = [...values].join(' ')
|
|
599
|
+
throw new Error(
|
|
600
|
+
`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}`
|
|
601
|
+
)
|
|
602
|
+
}
|
|
603
|
+
twClassesArray[indexOfModifier] = _.replace(twClassesArray[indexOfModifier], /{_applyProperties_}/, mergedProperties)
|
|
542
604
|
twClassesArray[indexOfModifier] = deduplicateLineProperties(twClassesArray[indexOfModifier])
|
|
543
605
|
}
|
|
544
606
|
})
|
|
@@ -546,6 +608,26 @@ export function compileApplyDirectives(twClasses) {
|
|
|
546
608
|
return twClassesArray.join('\n')
|
|
547
609
|
}
|
|
548
610
|
|
|
611
|
+
// Attempt to derive an alpha-applied semantic key from a TSS line whose value
|
|
612
|
+
// is a semantic name (e.g. `'.bg-surface': { backgroundColor: 'surfaceColor' }`).
|
|
613
|
+
// Returns the rewritten line with the derived key in place of the base name,
|
|
614
|
+
// or `null` when no candidate matches an entry in semantic.colors.json.
|
|
615
|
+
// Conflict errors from `deriveAlphaKey` propagate naturally.
|
|
616
|
+
function tryDeriveSemanticOpacity(targetLine, alphaPercent) {
|
|
617
|
+
const bodyMatch = targetLine.match(/\{([^}]*)\}/)
|
|
618
|
+
if (!bodyMatch) return null
|
|
619
|
+
const candidates = (bodyMatch[1].match(/'([^']+)'/g) || [])
|
|
620
|
+
.map(m => m.slice(1, -1))
|
|
621
|
+
.filter(v => !v.startsWith('#'))
|
|
622
|
+
for (const candidate of candidates) {
|
|
623
|
+
const derivedKey = deriveAlphaKey(candidate, alphaPercent)
|
|
624
|
+
if (derivedKey) {
|
|
625
|
+
return targetLine.replace(new RegExp(`'${candidate}'`, 'g'), `'${derivedKey}'`)
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
return null
|
|
629
|
+
}
|
|
630
|
+
|
|
549
631
|
/**
|
|
550
632
|
* Remove duplicate property keys in a TSS line, keeping the last occurrence.
|
|
551
633
|
* This ensures apply directives override static defaults (e.g. backgroundColor).
|
|
@@ -563,8 +645,8 @@ function deduplicateLineProperties(line) {
|
|
|
563
645
|
let depth = 0
|
|
564
646
|
let current = ''
|
|
565
647
|
for (const char of propsStr) {
|
|
566
|
-
if (char === '{') depth++
|
|
567
|
-
else if (char === '}') depth--
|
|
648
|
+
if (char === '{' || char === '[') depth++
|
|
649
|
+
else if (char === '}' || char === ']') depth--
|
|
568
650
|
else if (char === ',' && depth === 0) {
|
|
569
651
|
if (current.trim()) props.push(current.trim())
|
|
570
652
|
current = ''
|
|
@@ -593,13 +675,20 @@ export function justProperties(_foundClass) {
|
|
|
593
675
|
|
|
594
676
|
export function formatArbitraryValues(arbitraryValue, fromXMLs = false) {
|
|
595
677
|
const sign = (arbitraryValue.startsWith('-')) ? '-' : ''
|
|
596
|
-
const
|
|
678
|
+
const stripped = sign ? arbitraryValue.substring(1) : arbitraryValue
|
|
679
|
+
|
|
680
|
+
// Extract the value inside the last (...) so a negative value like top-(-10)
|
|
681
|
+
// is not mis-split by the hyphen inside the parentheses.
|
|
682
|
+
const parenMatch = stripped.match(/^(.+)-\(([^()]*)\)$/)
|
|
683
|
+
if (!parenMatch || parenMatch[2].trim() === '') {
|
|
684
|
+
return (fromXMLs) ? `// Property not yet supported: ${arbitraryValue}` : null
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
const ruleParts = parenMatch[1].split('-')
|
|
688
|
+
let value = parenMatch[2]
|
|
597
689
|
|
|
598
|
-
if (
|
|
599
|
-
|
|
600
|
-
} else if (splitedContent.length === 2) {
|
|
601
|
-
let rule = splitedContent.slice(0, 1).join('-')
|
|
602
|
-
let value = splitedContent[1].match(/(?<=\().*(?=\))/).pop()
|
|
690
|
+
if (ruleParts.length === 1) {
|
|
691
|
+
let rule = ruleParts[0]
|
|
603
692
|
|
|
604
693
|
if (rule === 'text') {
|
|
605
694
|
rule = (value.includes('#') || value.includes('rgb')) ? 'text-color' : 'text-size'
|
|
@@ -630,13 +719,12 @@ export function formatArbitraryValues(arbitraryValue, fromXMLs = false) {
|
|
|
630
719
|
? `'.${arbitraryValue}': { ` + _.replace(properties, /{value}/g, parseValue(value, sign)) + ' }'
|
|
631
720
|
: _.replace(properties, /{value}/g, parseValue(value, sign))
|
|
632
721
|
}
|
|
633
|
-
} else if (
|
|
634
|
-
const rule =
|
|
635
|
-
const value = splitedContent[2].match(/(?<=\().*(?=\))/).pop()
|
|
722
|
+
} else if (ruleParts.length === 2) {
|
|
723
|
+
const rule = ruleParts.join('-')
|
|
636
724
|
let properties = arbitraryValuesTable[rule]
|
|
637
725
|
|
|
638
726
|
if (properties) {
|
|
639
|
-
if (
|
|
727
|
+
if (ruleParts[0] === 'rounded') {
|
|
640
728
|
if (!value.includes(',')) {
|
|
641
729
|
properties = _.replace(properties, /{value1}/g, parseValue(parseValue(value) / 2, sign))
|
|
642
730
|
} else {
|
|
@@ -651,9 +739,8 @@ export function formatArbitraryValues(arbitraryValue, fromXMLs = false) {
|
|
|
651
739
|
? `'.${arbitraryValue}': { ` + _.replace(properties, /{value}/g, parseValue(value, sign)) + ' }'
|
|
652
740
|
: _.replace(properties, /{value}/g, parseValue(value, sign))
|
|
653
741
|
}
|
|
654
|
-
} else if (
|
|
655
|
-
const rule =
|
|
656
|
-
const value = splitedContent[3].match(/(?<=\().*(?=\))/).pop()
|
|
742
|
+
} else if (ruleParts.length === 3) {
|
|
743
|
+
const rule = ruleParts.join('-')
|
|
657
744
|
let properties = arbitraryValuesTable[rule]
|
|
658
745
|
|
|
659
746
|
if (properties) {
|
|
@@ -665,9 +752,8 @@ export function formatArbitraryValues(arbitraryValue, fromXMLs = false) {
|
|
|
665
752
|
? `'.${arbitraryValue}': { ` + _.replace(properties, /{value}/g, parseValue(value, sign)) + ' }'
|
|
666
753
|
: _.replace(properties, /{value}/g, parseValue(value, sign))
|
|
667
754
|
}
|
|
668
|
-
} else if (
|
|
669
|
-
const rule =
|
|
670
|
-
const value = splitedContent[4].match(/(?<=\().*(?=\))/).pop()
|
|
755
|
+
} else if (ruleParts.length === 4) {
|
|
756
|
+
const rule = ruleParts.join('-')
|
|
671
757
|
const properties = arbitraryValuesTable[rule]
|
|
672
758
|
|
|
673
759
|
if (properties) {
|
|
@@ -726,13 +812,22 @@ export function fixDuplicateKeys(compoundClasses) {
|
|
|
726
812
|
const cleanedStyles = []
|
|
727
813
|
const paddingObject = []
|
|
728
814
|
const backgroundGradientObject = []
|
|
815
|
+
const backgroundGradientDirection = []
|
|
729
816
|
|
|
730
817
|
_.each(compoundClasses, value => {
|
|
731
818
|
if (compoundClasses.length > 1) {
|
|
732
819
|
if (value.includes('font:')) {
|
|
733
820
|
fontObject.push(value.replace('font: ', '').replace(/{(.*)}/, '$1').trim())
|
|
734
|
-
} else if (value.includes('backgroundGradient:
|
|
735
|
-
|
|
821
|
+
} else if (value.includes('backgroundGradient:')) {
|
|
822
|
+
// Split into 2 buckets: colors (from-X/to-X) vs direction (bg-gradient-to-X with type/startPoint/endPoint).
|
|
823
|
+
// Both share the same property name `backgroundGradient` so they MUST be merged into a single object,
|
|
824
|
+
// otherwise the later one overwrites the earlier one in the emitted JS object literal.
|
|
825
|
+
const inner = value.replace('backgroundGradient: ', '').replace(/{(.*)}/, '$1').trim()
|
|
826
|
+
if (inner.startsWith('colors')) {
|
|
827
|
+
backgroundGradientObject.push(inner)
|
|
828
|
+
} else {
|
|
829
|
+
backgroundGradientDirection.push(inner)
|
|
830
|
+
}
|
|
736
831
|
} else if (value.includes('padding:')) {
|
|
737
832
|
paddingObject.push(value.replace('padding: ', '').replace(/{(.*)}/, '$1').trim())
|
|
738
833
|
} else {
|
|
@@ -763,13 +858,28 @@ export function fixDuplicateKeys(compoundClasses) {
|
|
|
763
858
|
cleanedStyles.push(`font: { ${fontObject.sort().join(', ')} }`)
|
|
764
859
|
}
|
|
765
860
|
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
861
|
+
// Merge gradient direction (bg-gradient-to-X) and gradient colors (from-X/to-X)
|
|
862
|
+
// into a single backgroundGradient object. They share the same property name,
|
|
863
|
+
// so emitting them as separate entries causes the later one to overwrite the earlier.
|
|
864
|
+
if (backgroundGradientDirection.length || backgroundGradientObject.length) {
|
|
865
|
+
let colorsPart = ''
|
|
866
|
+
if (backgroundGradientObject.length === 1) {
|
|
867
|
+
colorsPart = backgroundGradientObject[0]
|
|
868
|
+
} else if (backgroundGradientObject.length === 2) {
|
|
869
|
+
// from-X emits 2 colors (placeholder + actual), to-X emits 1.
|
|
870
|
+
// After sort() above, indices may swap depending on color name ordering,
|
|
871
|
+
// so identify by array length instead of position.
|
|
872
|
+
const colorsA = backgroundGradientObject[0].replace('colors: ', '').replace(/[[\]']+/g, '').trim().split(',').map(c => c.trim())
|
|
873
|
+
const colorsB = backgroundGradientObject[1].replace('colors: ', '').replace(/[[\]']+/g, '').trim().split(',').map(c => c.trim())
|
|
874
|
+
const fromEntry = colorsA.length === 2 ? colorsA : colorsB
|
|
875
|
+
const toEntry = colorsA.length === 1 ? colorsA : colorsB
|
|
876
|
+
colorsPart = `colors: [ '${toEntry[0]}', '${fromEntry[1]}' ]`
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
const parts = []
|
|
880
|
+
if (backgroundGradientDirection.length) parts.push(backgroundGradientDirection[0])
|
|
881
|
+
if (colorsPart) parts.push(colorsPart)
|
|
882
|
+
cleanedStyles.push(`backgroundGradient: { ${parts.join(', ')} }`)
|
|
773
883
|
}
|
|
774
884
|
|
|
775
885
|
// Missing properties to process
|