purgetss 7.5.3 → 7.6.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 +38 -17
- package/bin/purgetss +140 -1
- package/dist/purgetss.ui.js +23 -26
- package/dist/utilities.tss +13 -1
- package/lib/completions/titanium/completions-v3.json +62 -1
- package/lib/templates/purgetss.config.js.cjs +15 -1
- package/lib/templates/purgetss.ui.js.cjs +22 -25
- package/package.json +3 -1
- package/src/cli/commands/brand.js +69 -0
- package/src/cli/commands/create.js +11 -7
- package/src/cli/commands/fonts.js +9 -9
- package/src/cli/commands/icon-library.js +18 -16
- package/src/cli/commands/images.js +116 -0
- package/src/cli/commands/init.js +4 -0
- package/src/cli/commands/module.js +4 -2
- package/src/cli/commands/purge.js +48 -98
- package/src/cli/commands/semantic.js +180 -0
- package/src/cli/commands/shades.js +332 -13
- package/src/cli/utils/project-detection.js +4 -2
- package/src/core/analyzers/class-extractor.js +110 -3
- package/src/core/branding/brand-config.js +111 -0
- package/src/core/branding/branding-logger.js +40 -0
- package/src/core/branding/cleanup-legacy.js +220 -0
- package/src/core/branding/ensure-brand-section.js +80 -0
- package/src/core/branding/gen-android-adaptive.js +116 -0
- package/src/core/branding/gen-android-legacy.js +63 -0
- package/src/core/branding/gen-ic-launcher-xml.js +29 -0
- package/src/core/branding/gen-ios-dark.js +70 -0
- package/src/core/branding/gen-ios-tinted.js +55 -0
- package/src/core/branding/gen-ios.js +69 -0
- package/src/core/branding/gen-marketplace.js +71 -0
- package/src/core/branding/gen-notification.js +76 -0
- package/src/core/branding/gen-splash.js +64 -0
- package/src/core/branding/index.js +336 -0
- package/src/core/branding/post-gen-notes.js +145 -0
- package/src/core/branding/prepare-master.js +108 -0
- package/src/core/branding/tiapp-reader.js +110 -0
- package/src/core/images/ensure-images-section.js +57 -0
- package/src/core/images/gen-scales.js +181 -0
- package/src/core/images/index.js +171 -0
- package/src/shared/config-manager.js +46 -0
- package/src/shared/config-writer.js +84 -0
- package/src/shared/constants.js +3 -0
- package/src/shared/logger.js +69 -4
- package/src/shared/prompt.js +64 -0
- package/src/shared/svg-utils.js +80 -0
- package/src/shared/utils.js +8 -4
|
@@ -100,7 +100,7 @@ function copyFreeFonts() {
|
|
|
100
100
|
fs.copyFile(srcFonts_Folder + '/FontAwesome7Free-Regular.ttf', projectsFontsFolder + '/FontAwesome7Free-Regular.ttf', callback)
|
|
101
101
|
fs.copyFile(srcFonts_Folder + '/FontAwesome7Free-Solid.ttf', projectsFontsFolder + '/FontAwesome7Free-Solid.ttf', callback)
|
|
102
102
|
|
|
103
|
-
logger.
|
|
103
|
+
logger.item(chalk.green('Font Awesome Free'))
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
/**
|
|
@@ -111,7 +111,7 @@ function copyFreeFonts() {
|
|
|
111
111
|
function copyProFonts(fontFamilies, webFonts) {
|
|
112
112
|
_.each(fontFamilies, (dest, src) => {
|
|
113
113
|
if (copyFile(`${webFonts}/${src}`, dest)) {
|
|
114
|
-
logger.
|
|
114
|
+
logger.item(`${dest} copied to ${chalk.yellow('./app/assets/fonts')}`)
|
|
115
115
|
}
|
|
116
116
|
})
|
|
117
117
|
}
|
|
@@ -133,7 +133,7 @@ function copyMaterialIconsFonts() {
|
|
|
133
133
|
copyFile(`${srcFonts_Folder}/${familyName}`, familyName)
|
|
134
134
|
})
|
|
135
135
|
|
|
136
|
-
logger.
|
|
136
|
+
logger.item(chalk.green('Material Icons'))
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
/**
|
|
@@ -151,7 +151,7 @@ function copyMaterialSymbolsFonts() {
|
|
|
151
151
|
copyFile(`${srcFonts_Folder}/${familyName}`, familyName)
|
|
152
152
|
})
|
|
153
153
|
|
|
154
|
-
logger.
|
|
154
|
+
logger.item(chalk.green('Material Symbols'))
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
/**
|
|
@@ -160,7 +160,7 @@ function copyMaterialSymbolsFonts() {
|
|
|
160
160
|
function copyFramework7IconsFonts() {
|
|
161
161
|
// Framework7 Font
|
|
162
162
|
copyFile(srcFonts_Folder + '/Framework7-Icons.ttf', 'Framework7-Icons.ttf')
|
|
163
|
-
logger.
|
|
163
|
+
logger.item(chalk.green('Framework 7'))
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
/**
|
|
@@ -170,7 +170,7 @@ function copyFramework7IconsFonts() {
|
|
|
170
170
|
function buildFontAwesomeJS() {
|
|
171
171
|
// This function should be imported from the fonts module
|
|
172
172
|
// For now, just log that it would be called
|
|
173
|
-
logger.
|
|
173
|
+
logger.item(chalk.yellow('Font Awesome JS module would be built'))
|
|
174
174
|
}
|
|
175
175
|
|
|
176
176
|
/**
|
|
@@ -218,23 +218,23 @@ function copyFontLibrary(vendor) {
|
|
|
218
218
|
buildFontAwesomeJS()
|
|
219
219
|
} else {
|
|
220
220
|
fs.copyFileSync(srcLibFA, projectsLibFolder + '/fontawesome.js')
|
|
221
|
-
logger.
|
|
221
|
+
logger.item(chalk.yellow('fontawesome.js'))
|
|
222
222
|
}
|
|
223
223
|
break
|
|
224
224
|
case 'mi':
|
|
225
225
|
case 'materialicons':
|
|
226
226
|
fs.copyFileSync(srcLibMI, projectsLibFolder + '/materialicons.js')
|
|
227
|
-
logger.
|
|
227
|
+
logger.item(chalk.yellow('materialicons.js'))
|
|
228
228
|
break
|
|
229
229
|
case 'ms':
|
|
230
230
|
case 'materialsymbol':
|
|
231
231
|
fs.copyFileSync(srcLibMS, projectsLibFolder + '/materialsymbols.js')
|
|
232
|
-
logger.
|
|
232
|
+
logger.item(chalk.yellow('materialsymbols.js'))
|
|
233
233
|
break
|
|
234
234
|
case 'f7':
|
|
235
235
|
case 'framework7':
|
|
236
236
|
fs.copyFileSync(srcLibF7, projectsLibFolder + '/framework7icons.js')
|
|
237
|
-
logger.
|
|
237
|
+
logger.item(chalk.yellow('framework7icons.js'))
|
|
238
238
|
break
|
|
239
239
|
}
|
|
240
240
|
}
|
|
@@ -251,23 +251,23 @@ function copyFontStyle(vendor) {
|
|
|
251
251
|
buildFontAwesomeJS()
|
|
252
252
|
} else {
|
|
253
253
|
fs.copyFileSync(srcFontAwesomeTSSFile, projectsPurge_TSS_Styles_Folder + '/fontawesome.tss')
|
|
254
|
-
logger.
|
|
254
|
+
logger.item(chalk.yellow('fontawesome.tss'))
|
|
255
255
|
}
|
|
256
256
|
break
|
|
257
257
|
case 'mi':
|
|
258
258
|
case 'materialicons':
|
|
259
259
|
fs.copyFileSync(srcMaterialIconsTSSFile, projectsPurge_TSS_Styles_Folder + '/materialicons.tss')
|
|
260
|
-
logger.
|
|
260
|
+
logger.item(chalk.yellow('materialicons.tss'))
|
|
261
261
|
break
|
|
262
262
|
case 'ms':
|
|
263
263
|
case 'materialsymbol':
|
|
264
264
|
fs.copyFileSync(srcMaterialSymbolsTSSFile, projectsPurge_TSS_Styles_Folder + '/materialsymbols.tss')
|
|
265
|
-
logger.
|
|
265
|
+
logger.item(chalk.yellow('materialsymbols.tss'))
|
|
266
266
|
break
|
|
267
267
|
case 'f7':
|
|
268
268
|
case 'framework7':
|
|
269
269
|
fs.copyFileSync(srcFramework7FontTSSFile, projectsPurge_TSS_Styles_Folder + '/framework7icons.tss')
|
|
270
|
-
logger.
|
|
270
|
+
logger.item(chalk.yellow('framework7icons.tss'))
|
|
271
271
|
break
|
|
272
272
|
}
|
|
273
273
|
}
|
|
@@ -391,8 +391,10 @@ export async function copyModulesLibrary() {
|
|
|
391
391
|
logger.info(chalk.yellow('purgetss.ui'), 'module copied to', chalk.yellow('./Resources/lib'), 'folder')
|
|
392
392
|
return true
|
|
393
393
|
} else {
|
|
394
|
-
logger.
|
|
395
|
-
|
|
394
|
+
logger.block(
|
|
395
|
+
`Please make sure you are running ${chalk.green('purgetss')} within an Alloy or Classic Project.`,
|
|
396
|
+
`For more information, visit ${chalk.green('https://purgetss.com')}`
|
|
397
|
+
)
|
|
396
398
|
return false
|
|
397
399
|
}
|
|
398
400
|
} catch (error) {
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PurgeTSS - Images Command
|
|
3
|
+
*
|
|
4
|
+
* Generates multi-density variants of UI images for Titanium Alloy or Classic
|
|
5
|
+
* projects. Auto-discovers sources in `./purgetss/images/` by default; accepts
|
|
6
|
+
* a path argument to override (file or directory).
|
|
7
|
+
*
|
|
8
|
+
* Precedence for every option: CLI flag > `images:` section in config > default.
|
|
9
|
+
*
|
|
10
|
+
* @fileoverview Assets command entry point
|
|
11
|
+
* @author César Estrada
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import fs from 'fs'
|
|
15
|
+
import path from 'path'
|
|
16
|
+
import chalk from 'chalk'
|
|
17
|
+
import { runImages } from '../../core/images/index.js'
|
|
18
|
+
import { logger } from '../../core/branding/branding-logger.js'
|
|
19
|
+
import { ensureImagesSection } from '../../core/images/ensure-images-section.js'
|
|
20
|
+
import { getConfigFile } from '../../shared/config-manager.js'
|
|
21
|
+
import { projectsPurge_TSS_Images_Folder } from '../../shared/constants.js'
|
|
22
|
+
|
|
23
|
+
const VALID_FORMATS = new Set(['webp', 'jpeg', 'jpg', 'png', 'avif', 'gif', 'tiff'])
|
|
24
|
+
|
|
25
|
+
export async function images(cliSource, options = {}) {
|
|
26
|
+
if (options.debug) logger.setDebugMode(true)
|
|
27
|
+
|
|
28
|
+
const projectRoot = options.project ? path.resolve(options.project) : process.cwd()
|
|
29
|
+
|
|
30
|
+
if (!options.project) ensureImagesSection()
|
|
31
|
+
|
|
32
|
+
const cfg = loadImagesSection()
|
|
33
|
+
|
|
34
|
+
// --android and --ios are mutually exclusive.
|
|
35
|
+
if (options.android && options.ios) {
|
|
36
|
+
logger.error('--android and --ios are mutually exclusive. Pass neither to generate both, or pick one.')
|
|
37
|
+
process.exit(1)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const format = options.format ?? cfg.format ?? null
|
|
41
|
+
if (format && !VALID_FORMATS.has(format.toLowerCase())) {
|
|
42
|
+
logger.error(`Invalid --format '${format}'. Valid: ${[...VALID_FORMATS].join(', ')}`)
|
|
43
|
+
process.exit(1)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const source = resolveSource(cliSource, projectRoot)
|
|
47
|
+
if (!source) {
|
|
48
|
+
printMissingSourceHelp(projectRoot)
|
|
49
|
+
process.exit(1)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
await runImages({
|
|
54
|
+
source,
|
|
55
|
+
projectRoot,
|
|
56
|
+
androidOnly: Boolean(options.android),
|
|
57
|
+
iphoneOnly: Boolean(options.ios),
|
|
58
|
+
format: format ? format.toLowerCase() : null,
|
|
59
|
+
quality: options.quality ?? cfg.quality ?? 85,
|
|
60
|
+
dryRun: Boolean(options.dryRun),
|
|
61
|
+
yes: Boolean(options.yes),
|
|
62
|
+
confirmOverwrites: cfg.confirmOverwrites !== false
|
|
63
|
+
})
|
|
64
|
+
} catch (err) {
|
|
65
|
+
logger.error(err.message)
|
|
66
|
+
if (options.debug) console.error(err.stack)
|
|
67
|
+
process.exit(1)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function loadImagesSection() {
|
|
72
|
+
try {
|
|
73
|
+
const cfg = getConfigFile()
|
|
74
|
+
if (cfg && typeof cfg.images === 'object') return cfg.images
|
|
75
|
+
} catch {}
|
|
76
|
+
return {}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function resolveSource(cliSource, projectRoot) {
|
|
80
|
+
const imagesFolder = projectRoot === process.cwd()
|
|
81
|
+
? projectsPurge_TSS_Images_Folder
|
|
82
|
+
: path.join(projectRoot, 'purgetss', 'images')
|
|
83
|
+
|
|
84
|
+
if (cliSource) {
|
|
85
|
+
if (path.isAbsolute(cliSource)) {
|
|
86
|
+
return fs.existsSync(cliSource) ? cliSource : null
|
|
87
|
+
}
|
|
88
|
+
// Relative paths: try purgetss/images/ first (convention), then cwd.
|
|
89
|
+
// Lets users write short paths like `background/pink.png` without the prefix.
|
|
90
|
+
const insideImages = path.resolve(imagesFolder, cliSource)
|
|
91
|
+
if (fs.existsSync(insideImages)) return insideImages
|
|
92
|
+
|
|
93
|
+
const cwdResolved = path.resolve(projectRoot, cliSource)
|
|
94
|
+
if (fs.existsSync(cwdResolved)) return cwdResolved
|
|
95
|
+
|
|
96
|
+
return null
|
|
97
|
+
}
|
|
98
|
+
return fs.existsSync(imagesFolder) ? imagesFolder : null
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function printMissingSourceHelp(projectRoot) {
|
|
102
|
+
const rel = (p) => path.relative(projectRoot, p) || '.'
|
|
103
|
+
const imagesDir = path.join(projectRoot, 'purgetss', 'images')
|
|
104
|
+
|
|
105
|
+
logger.error('No source images found.')
|
|
106
|
+
console.log()
|
|
107
|
+
console.log(` Expected images inside ${chalk.cyan(rel(imagesDir) + '/')}.`)
|
|
108
|
+
console.log(` The folder already exists — drop your images into it (subdirectories are preserved):`)
|
|
109
|
+
console.log(` ${chalk.cyan('cp my-ui-asset.png ' + rel(imagesDir) + '/')}`)
|
|
110
|
+
console.log()
|
|
111
|
+
console.log(' Alternatives:')
|
|
112
|
+
console.log(` ${chalk.gray('•')} Pass a file or directory explicitly:`)
|
|
113
|
+
console.log(` ${chalk.cyan('purgetss images ./docs/screenshots')}`)
|
|
114
|
+
console.log(` ${chalk.cyan('purgetss images ./logo.png')}`)
|
|
115
|
+
console.log()
|
|
116
|
+
}
|
package/src/cli/commands/init.js
CHANGED
|
@@ -21,6 +21,8 @@ import {
|
|
|
21
21
|
projectsAlloyJMKFile,
|
|
22
22
|
projectsPurgeTSSFolder,
|
|
23
23
|
projectsPurge_TSS_Fonts_Folder,
|
|
24
|
+
projectsPurge_TSS_Brand_Folder,
|
|
25
|
+
projectsPurge_TSS_Images_Folder,
|
|
24
26
|
srcConfigFile,
|
|
25
27
|
projectsFA_TSS_File,
|
|
26
28
|
srcFontAwesomeTSSFile,
|
|
@@ -70,6 +72,8 @@ export function createConfigFile() {
|
|
|
70
72
|
if (alloyProject()) {
|
|
71
73
|
makeSureFolderExists(projectsPurgeTSSFolder)
|
|
72
74
|
makeSureFolderExists(projectsPurge_TSS_Fonts_Folder)
|
|
75
|
+
makeSureFolderExists(projectsPurge_TSS_Brand_Folder)
|
|
76
|
+
makeSureFolderExists(projectsPurge_TSS_Images_Folder)
|
|
73
77
|
|
|
74
78
|
if (fs.existsSync(projectsConfigJS)) {
|
|
75
79
|
logger.warn('./purgetss/config.cjs', chalk.red('file already exists!'))
|
|
@@ -41,8 +41,10 @@ export function copyModulesLibrary() {
|
|
|
41
41
|
return true
|
|
42
42
|
} else {
|
|
43
43
|
// Not in a valid project
|
|
44
|
-
logger.
|
|
45
|
-
|
|
44
|
+
logger.block(
|
|
45
|
+
`Please make sure you are running ${chalk.green('purgetss')} within an Alloy or Classic Project.`,
|
|
46
|
+
`For more information, visit ${chalk.green('https://purgetss.com')}`
|
|
47
|
+
)
|
|
46
48
|
return false
|
|
47
49
|
}
|
|
48
50
|
}
|
|
@@ -31,6 +31,7 @@ import { init } from './init.js'
|
|
|
31
31
|
import { getConfigOptions, getConfigFile, ensureConfig } from '../../shared/config-manager.js'
|
|
32
32
|
|
|
33
33
|
// Import purger functions from core modules
|
|
34
|
+
import { processControllers } from '../../core/analyzers/class-extractor.js'
|
|
34
35
|
import { purgeTailwind } from '../../core/purger/tailwind-purger.js'
|
|
35
36
|
import {
|
|
36
37
|
purgeFontAwesome,
|
|
@@ -385,12 +386,16 @@ function throwPreValidationError({ relativePath, lineNumber, lineContent, messag
|
|
|
385
386
|
error.lineNumber = lineNumber
|
|
386
387
|
error.lineContent = lineContent
|
|
387
388
|
|
|
388
|
-
logger.
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
389
|
+
logger.block(
|
|
390
|
+
chalk.red('XML Syntax Error'),
|
|
391
|
+
`File: ${chalk.yellow(`"${relativePath}"`)}`,
|
|
392
|
+
`Line: ${chalk.yellow(lineNumber)}`,
|
|
393
|
+
`Content: ${chalk.yellow(`"${lineContent}"`)}`,
|
|
394
|
+
'',
|
|
395
|
+
chalk.red(message),
|
|
396
|
+
'',
|
|
397
|
+
`${chalk.green('Fix:')} ${fix}`
|
|
398
|
+
)
|
|
394
399
|
|
|
395
400
|
throw error
|
|
396
401
|
}
|
|
@@ -412,69 +417,6 @@ function extractClasses(currentText, currentFile) {
|
|
|
412
417
|
}
|
|
413
418
|
}
|
|
414
419
|
|
|
415
|
-
/**
|
|
416
|
-
* Process controller files for classes
|
|
417
|
-
* COPIED exactly from original processControllers() function
|
|
418
|
-
*/
|
|
419
|
-
function processControllers(data) {
|
|
420
|
-
const allWords = []
|
|
421
|
-
const lines = data.split(/\r?\n/)
|
|
422
|
-
|
|
423
|
-
lines.forEach(line => {
|
|
424
|
-
const words = extractWordsFromLine(line)
|
|
425
|
-
if (words.length > 0) {
|
|
426
|
-
allWords.push(...words)
|
|
427
|
-
}
|
|
428
|
-
})
|
|
429
|
-
|
|
430
|
-
return allWords
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
/**
|
|
434
|
-
* Extract words from a line of controller code
|
|
435
|
-
* COPIED exactly from original extractWordsFromLine() function
|
|
436
|
-
*/
|
|
437
|
-
function extractWordsFromLine(line) {
|
|
438
|
-
const patterns = [
|
|
439
|
-
{
|
|
440
|
-
// apply: 'classes'
|
|
441
|
-
regex: /apply:\s*'([^']+)'/,
|
|
442
|
-
process: match => match[1].split(/\s+/)
|
|
443
|
-
},
|
|
444
|
-
{
|
|
445
|
-
// classes: ['class1', 'class2'] o classes: ['class1 class2']
|
|
446
|
-
regex: /classes:\s*\[([^\]]+)\]/,
|
|
447
|
-
process: match => match[1].split(',').map(item => item.trim().replace(/['"]/g, ''))
|
|
448
|
-
},
|
|
449
|
-
{
|
|
450
|
-
// classes: 'class1 class2'
|
|
451
|
-
regex: /classes:\s*'([^']+)'/,
|
|
452
|
-
process: match => match[1].split(/\s+/)
|
|
453
|
-
}
|
|
454
|
-
]
|
|
455
|
-
|
|
456
|
-
// Process simple patterns
|
|
457
|
-
const words = patterns.reduce((acc, { regex, process }) => {
|
|
458
|
-
const match = regex.exec(line)
|
|
459
|
-
return match ? [...acc, ...process(match)] : acc
|
|
460
|
-
}, [])
|
|
461
|
-
|
|
462
|
-
// Process addClass, removeClass, resetClass
|
|
463
|
-
const classFunctionRegex = /(?:\.\w+Class|resetClass)\([^,]+,\s*(?:'([^']+)'|\[([^\]]+)\])/g
|
|
464
|
-
let classFunctionMatch
|
|
465
|
-
while ((classFunctionMatch = classFunctionRegex.exec(line)) !== null) {
|
|
466
|
-
const content = classFunctionMatch[1] || classFunctionMatch[2]
|
|
467
|
-
if (content) {
|
|
468
|
-
const classes = content.includes(',')
|
|
469
|
-
? content.split(',').map(item => item.trim().replace(/['"]/g, ''))
|
|
470
|
-
: content.replace(/['"]/g, '').split(/\s+/)
|
|
471
|
-
words.push(...classes)
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
return words
|
|
476
|
-
}
|
|
477
|
-
|
|
478
420
|
/**
|
|
479
421
|
* Filter invalid characters from class names
|
|
480
422
|
* COPIED exactly from original filterCharacters() function
|
|
@@ -737,49 +679,57 @@ export function purgeClasses(options) {
|
|
|
737
679
|
|
|
738
680
|
if (Date.now() > (fs.statSync(projectsAppTSS).mtimeMs + 2000) || recentlyCreated) {
|
|
739
681
|
start()
|
|
740
|
-
|
|
741
|
-
init(options)
|
|
742
|
-
|
|
743
|
-
backupOriginalAppTss()
|
|
744
|
-
|
|
745
|
-
let uniqueClasses
|
|
746
|
-
|
|
682
|
+
logger.startSection()
|
|
747
683
|
try {
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
684
|
+
// Explicit header so every purge run shows which project is being
|
|
685
|
+
// processed — mirrors the Auto-Purging line emitted by the alloy.jmk hook.
|
|
686
|
+
logger.info('Purging', chalk.yellow(cwd))
|
|
687
|
+
|
|
688
|
+
init(options)
|
|
689
|
+
|
|
690
|
+
backupOriginalAppTss()
|
|
691
|
+
|
|
692
|
+
let uniqueClasses
|
|
693
|
+
|
|
694
|
+
try {
|
|
695
|
+
uniqueClasses = getUniqueClasses()
|
|
696
|
+
} catch (error) {
|
|
697
|
+
// Handle pre-validation errors (XML syntax errors detected before parsing)
|
|
698
|
+
if (error.isPreValidationError) {
|
|
699
|
+
// Error already printed by preValidateXML, exit cleanly
|
|
700
|
+
// eslint-disable-next-line n/no-process-exit
|
|
701
|
+
process.exit(1)
|
|
702
|
+
}
|
|
703
|
+
// Re-throw other errors
|
|
704
|
+
throw error
|
|
755
705
|
}
|
|
756
|
-
// Re-throw other errors
|
|
757
|
-
throw error
|
|
758
|
-
}
|
|
759
706
|
|
|
760
|
-
|
|
707
|
+
let tempPurged = copyResetTemplateAnd_appTSS()
|
|
761
708
|
|
|
762
|
-
|
|
709
|
+
tempPurged += purgeTailwind(uniqueClasses, purgingDebug)
|
|
763
710
|
|
|
764
|
-
|
|
711
|
+
const cleanUniqueClasses = cleanClasses(uniqueClasses)
|
|
765
712
|
|
|
766
|
-
|
|
713
|
+
tempPurged += purgeFontAwesome(uniqueClasses, cleanUniqueClasses, purgingDebug)
|
|
767
714
|
|
|
768
|
-
|
|
715
|
+
tempPurged += purgeMaterialIcons(uniqueClasses, cleanUniqueClasses, purgingDebug)
|
|
769
716
|
|
|
770
|
-
|
|
717
|
+
tempPurged += purgeMaterialSymbols(uniqueClasses, cleanUniqueClasses, purgingDebug)
|
|
771
718
|
|
|
772
|
-
|
|
719
|
+
tempPurged += purgeFramework7(uniqueClasses, cleanUniqueClasses, purgingDebug)
|
|
773
720
|
|
|
774
|
-
|
|
721
|
+
tempPurged += purgeFonts(uniqueClasses, cleanUniqueClasses, purgingDebug)
|
|
775
722
|
|
|
776
|
-
|
|
723
|
+
tempPurged += processMissingClasses(tempPurged)
|
|
777
724
|
|
|
778
|
-
|
|
725
|
+
saveFile(projectsAppTSS, tempPurged)
|
|
779
726
|
|
|
780
|
-
|
|
727
|
+
logger.file('app.tss')
|
|
781
728
|
|
|
782
|
-
|
|
729
|
+
finish()
|
|
730
|
+
} finally {
|
|
731
|
+
logger.endSection()
|
|
732
|
+
}
|
|
783
733
|
|
|
784
734
|
return true
|
|
785
735
|
} else {
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PurgeTSS - `semantic` command
|
|
3
|
+
*
|
|
4
|
+
* Generates Titanium semantic colors (app/assets/semantic.colors.json) in two
|
|
5
|
+
* modes, dispatched by --single:
|
|
6
|
+
*
|
|
7
|
+
* Palette mode (no --single):
|
|
8
|
+
* One base hex → 11-step tonal palette with mirror-by-index Light/Dark
|
|
9
|
+
* inversion (anchored at shade 500). Writes JSON + config mapping (the
|
|
10
|
+
* 11 shade keys are mechanically derived from the family name, so the
|
|
11
|
+
* class mapping is unambiguous).
|
|
12
|
+
*
|
|
13
|
+
* Single mode (--single):
|
|
14
|
+
* Explicit light + optional dark + optional alpha → one purpose-based
|
|
15
|
+
* semantic color. Writes JSON ONLY. The class name mapping in config.cjs
|
|
16
|
+
* is a design-layer decision (designers pick `bg-surface` to point at
|
|
17
|
+
* `surfaceColor`, `text-on-surface` to point at `textColor`, etc.) and
|
|
18
|
+
* should not be auto-derived from the semantic key. The command tells
|
|
19
|
+
* the user how to add the mapping.
|
|
20
|
+
*
|
|
21
|
+
* If the name matches an existing palette shade (e.g. `amazon50` while
|
|
22
|
+
* palette `amazon` exists), the operation narrows to an in-place JSON
|
|
23
|
+
* value edit — no new top-level entry, no config touch.
|
|
24
|
+
*
|
|
25
|
+
* @author César Estrada
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import chalk from 'chalk'
|
|
29
|
+
import { alloyProject } from '../../shared/utils.js'
|
|
30
|
+
import { logger } from '../../shared/logger.js'
|
|
31
|
+
import { ensureConfig, getConfigFile } from '../../shared/config-manager.js'
|
|
32
|
+
import {
|
|
33
|
+
toCamelCase,
|
|
34
|
+
buildSemanticPalette,
|
|
35
|
+
buildSingleSemantic,
|
|
36
|
+
writeSemanticColors,
|
|
37
|
+
writeSemanticJSON,
|
|
38
|
+
writeConfigMapping,
|
|
39
|
+
updateSemanticEntry,
|
|
40
|
+
wrapHexWithAlpha,
|
|
41
|
+
detectFamilyShadeConflict,
|
|
42
|
+
normalizeAlpha,
|
|
43
|
+
checkIfColorModule,
|
|
44
|
+
missingHexMessage
|
|
45
|
+
} from './shades.js'
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Normalize a user-supplied name into both kebab and camel forms while
|
|
49
|
+
* preserving the user's case. Handles three input shapes:
|
|
50
|
+
* - camelCase ('surfaceColor') → { kebab: 'surface-color', camel: 'surfaceColor' }
|
|
51
|
+
* - kebab ('surface-color') → { kebab: 'surface-color', camel: 'surfaceColor' }
|
|
52
|
+
* - natural ('Surface Color') → { kebab: 'surface-color', camel: 'surfaceColor' }
|
|
53
|
+
*/
|
|
54
|
+
function parseName(raw) {
|
|
55
|
+
const clean = String(raw).replace(/'/g, '').replace(/\//g, '').trim()
|
|
56
|
+
if (/\s/.test(clean)) {
|
|
57
|
+
const kebab = clean.toLowerCase().split(/\s+/).join('-')
|
|
58
|
+
return { kebab, camel: toCamelCase(kebab) }
|
|
59
|
+
}
|
|
60
|
+
if (clean.includes('-')) {
|
|
61
|
+
const kebab = clean.toLowerCase()
|
|
62
|
+
return { kebab, camel: toCamelCase(kebab) }
|
|
63
|
+
}
|
|
64
|
+
// No space, no hyphen: treat as camelCase or single word, preserve case
|
|
65
|
+
const camel = clean
|
|
66
|
+
const kebab = camel.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase()
|
|
67
|
+
return { kebab, camel }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function semantic(args, options) {
|
|
71
|
+
const silent = options.log
|
|
72
|
+
|
|
73
|
+
if (options.single) {
|
|
74
|
+
return runSingle(args, options, silent)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return runPalette(args, options, silent)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function runPalette(args, options, silent) {
|
|
81
|
+
if (!args.hexcode && !options.random) {
|
|
82
|
+
logger.block(...missingHexMessage('semantic'))
|
|
83
|
+
return false
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const chroma = (await import('chroma-js')).default
|
|
87
|
+
const referenceColorFamilies = (await import('../../../lib/color-shades/tailwindColors.js')).default
|
|
88
|
+
const generateColorShades = (await import('../../../lib/color-shades/generateColorShades.js')).default
|
|
89
|
+
|
|
90
|
+
const colorFamily = options.random
|
|
91
|
+
? generateColorShades(chroma.random(), referenceColorFamilies)
|
|
92
|
+
: generateColorShades(args.hexcode, referenceColorFamilies)
|
|
93
|
+
|
|
94
|
+
if (args.name) colorFamily.name = args.name
|
|
95
|
+
else if (options.name) colorFamily.name = options.name
|
|
96
|
+
|
|
97
|
+
colorFamily.name = colorFamily.name.replace(/'/g, '').replace(/\//g, '').replace(/\s+/g, ' ')
|
|
98
|
+
|
|
99
|
+
const { kebab: kebabName } = parseName(colorFamily.name)
|
|
100
|
+
const { semanticEntries, configMapping } = buildSemanticPalette(colorFamily, kebabName)
|
|
101
|
+
|
|
102
|
+
if (alloyProject(silent) && !silent) {
|
|
103
|
+
ensureConfig()
|
|
104
|
+
writeSemanticColors(semanticEntries, kebabName, configMapping, options)
|
|
105
|
+
logger.info(`${chalk.hex(colorFamily.hexcode).bold(`"${colorFamily.name}"`)} palette (11 shades) saved to`, chalk.yellow('app/assets/semantic.colors.json'))
|
|
106
|
+
} else {
|
|
107
|
+
logger.info(`${chalk.hex(colorFamily.hexcode).bold(`"${colorFamily.name}"`)} palette preview:\n${JSON.stringify(semanticEntries, null, 2)}`)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return true
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function runSingle(args, options, silent) {
|
|
114
|
+
const lightHex = args.hexcode
|
|
115
|
+
const rawName = args.name || options.name
|
|
116
|
+
if (!lightHex) {
|
|
117
|
+
logger.info(`${chalk.red('Missing light hex.')} Usage: ${chalk.green("pt semantic --single '#F9FAFB' surfaceColor [--dark '#0f172a'] [--alpha 50]")}`)
|
|
118
|
+
return false
|
|
119
|
+
}
|
|
120
|
+
if (!rawName) {
|
|
121
|
+
logger.info(`${chalk.red('Missing name.')} Usage: ${chalk.green("pt semantic --single '#F9FAFB' surfaceColor [--dark '#0f172a'] [--alpha 50]")}`)
|
|
122
|
+
return false
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const darkHex = options.dark || lightHex
|
|
126
|
+
const alpha = normalizeAlpha(options.alpha)
|
|
127
|
+
const { kebab: kebabName, camel: camelName } = parseName(rawName)
|
|
128
|
+
|
|
129
|
+
// Preview mode: log the JSON, no writes, no Alloy check
|
|
130
|
+
if (silent) {
|
|
131
|
+
const { semanticEntries } = buildSingleSemantic(camelName, lightHex, darkHex, alpha)
|
|
132
|
+
logger.info(`${chalk.hex(lightHex).bold(`"${camelName}"`)} preview:\n${JSON.stringify(semanticEntries, null, 2)}`)
|
|
133
|
+
return true
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!alloyProject(silent)) return false
|
|
137
|
+
ensureConfig()
|
|
138
|
+
|
|
139
|
+
// If the name matches an existing palette shade (e.g. `amazon50` when palette
|
|
140
|
+
// `amazon` is already declared), interpret as an in-place value edit. Update
|
|
141
|
+
// the JSON entry, leave config.cjs alone (palette already maps to this key).
|
|
142
|
+
const conflict = detectFamilyShadeConflict(getConfigFile(), kebabName, camelName)
|
|
143
|
+
if (conflict) {
|
|
144
|
+
const value = {
|
|
145
|
+
light: wrapHexWithAlpha(lightHex, alpha),
|
|
146
|
+
dark: wrapHexWithAlpha(darkHex, alpha)
|
|
147
|
+
}
|
|
148
|
+
updateSemanticEntry(conflict.camelKey, value)
|
|
149
|
+
checkIfColorModule()
|
|
150
|
+
logger.info(`${chalk.hex(lightHex).bold(conflict.camelKey)} updated in ${chalk.yellow('app/assets/semantic.colors.json')} — palette ${chalk.yellow(conflict.parentName)} already references this key, config.cjs left unchanged.`)
|
|
151
|
+
return true
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Fresh single entry: write JSON + auto-map config.cjs to a sensible class name.
|
|
155
|
+
// Convention: strip the trailing 'Color' suffix (Titanium's semantic naming
|
|
156
|
+
// pattern) and kebab-case the rest. Users who want a different class name
|
|
157
|
+
// (`on-surface` for `textColor`, etc.) edit config.cjs after the fact —
|
|
158
|
+
// overriding the auto-mapping is one keystroke; typing it from scratch is many.
|
|
159
|
+
const { semanticEntries } = buildSingleSemantic(camelName, lightHex, darkHex, alpha)
|
|
160
|
+
const className = suggestClassName(camelName)
|
|
161
|
+
writeSemanticJSON(semanticEntries, camelName)
|
|
162
|
+
writeConfigMapping(className, camelName, options)
|
|
163
|
+
logger.info(`${chalk.hex(lightHex).bold(`"${camelName}"`)} saved to ${chalk.yellow('app/assets/semantic.colors.json')} and mapped to class ${chalk.green(className)} in ${chalk.yellow('config.cjs')}.`)
|
|
164
|
+
return true
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Derive a class name from a Titanium semantic key by stripping the
|
|
169
|
+
* conventional `Color` suffix and kebab-casing the result.
|
|
170
|
+
* 'surfaceColor' → 'surface'
|
|
171
|
+
* 'surfaceHighColor' → 'surface-high'
|
|
172
|
+
* 'borderColor' → 'border'
|
|
173
|
+
* 'overlay' → 'overlay' (no Color suffix → unchanged)
|
|
174
|
+
*/
|
|
175
|
+
function suggestClassName(camelKey) {
|
|
176
|
+
const stripped = camelKey.replace(/Color$/, '') || camelKey
|
|
177
|
+
return stripped.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase()
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export default { semantic }
|