purgetss 7.7.0 → 7.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -1
- package/bin/purgetss +24 -7
- package/dist/purgetss.ui.js +1 -1
- package/lib/templates/purgetss.config.js.cjs +6 -15
- package/package.json +2 -2
- package/src/cli/commands/build.js +9 -4
- package/src/cli/commands/images.js +8 -0
- package/src/cli/commands/purge.js +17 -3
- package/src/cli/commands/shades.js +2 -2
- package/src/cli/utils/unsupported-class-reporter.js +209 -0
- package/src/core/branding/ensure-brand-section.js +6 -15
- package/src/core/branding/post-gen-notes.js +12 -4
- package/{experimental/completions2.js → src/core/builders/auto-utilities-builder.js} +56 -27
- package/src/core/builders/tailwind-builder.js +2 -2
- package/src/core/builders/tailwind-helpers.js +0 -444
- package/src/core/images/gen-scales.js +24 -6
- package/src/core/images/index.js +9 -2
- package/src/core/purger/tailwind-purger.js +40 -4
- package/src/shared/helpers/core.js +0 -19
- package/src/shared/helpers/utils.js +100 -28
- package/src/shared/semantic-helpers.js +143 -0
- package/src/dev/builders/tailwind-builder.js +0 -26
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
- 23,300+ utility classes for styling Titanium views
|
|
18
18
|
- Parses XML files to generate a clean `app.tss` with only the classes your project uses
|
|
19
19
|
- Customizable defaults via `config.cjs`, with JIT classes for arbitrary values
|
|
20
|
-
- `brand` command for Titanium icons and branding assets,
|
|
20
|
+
- `brand` command for Titanium icons and branding assets, focused on the modern Titanium icon pipeline: Android adaptive icons, iOS icon variants, optional Android 12+ splash artwork, and a minimal `default.png` compatibility fallback
|
|
21
21
|
- Icon font support: Font Awesome, Material Icons, Material Symbols, Framework7-Icons
|
|
22
22
|
- `build-fonts` command generates `fonts.tss` with class definitions and fontFamily selectors
|
|
23
23
|
- `shades` command generates color shades from any hex color
|
|
@@ -377,6 +377,14 @@ Button: {
|
|
|
377
377
|
|
|
378
378
|
## Recent changes
|
|
379
379
|
|
|
380
|
+
### v7.8.0
|
|
381
|
+
|
|
382
|
+
- **`--width <n>` flag for `images`.** Pin Android `mdpi` / iPhone `@1x` to a specific width in pixels (e.g. `purgetss images logo.svg --width 256`); larger scales derive at ×1.5, ×2, ×3, ×4 with height staying proportional. Recommended when the source is an SVG export from Affinity / Illustrator with a disproportionate viewBox — the legacy 4× master convention produces unpredictable sizes there, and the new flag pins the result exactly. When you pass an SVG without `--width`, `images` now prints a one-time hint suggesting the flag, then falls back to the legacy behavior.
|
|
383
|
+
- **Class syntax pre-validation.** `purgetss` now halts with a structured `Class Syntax Error` block (file + line + suggested fix) when it detects known authoring mistakes in your class names: inverted negative sign (`top-(-10)` → `-top-(10)`), Tailwind-style brackets (`top-[10px]` → `top-(10px)`), empty parentheses (`wh-()`), whitespace inside parentheses (`wh-( 200 )`), and redundant `px` unit (`top-(10px)` → `top-(10)`).
|
|
384
|
+
- All offenders are reported in a single run, so you can fix them in one pass.
|
|
385
|
+
- Generic unknown classes (typos, vendor utilities not enabled, custom classes not yet declared) are NOT flagged by the validator — they continue to flow silently into the `// Unused or unsupported classes` block in `app.tss`, exactly as in previous versions.
|
|
386
|
+
- **Arbitrary-value parser no longer crashes on negative values inside parentheses.** Classes like `top-(-10)`, `mt-(-5)`, or `origin-(-10,-20)` used to trigger a `Cannot read properties of null (reading 'pop')` exception. The parser was rewritten to extract the `(...)` portion first, so a `-` inside the value never breaks the split — and the new pre-validator catches the inverted-sign form before it gets that far.
|
|
387
|
+
|
|
380
388
|
### v7.7.0
|
|
381
389
|
|
|
382
390
|
- `brand` now uses grouped config sections: `brand.logos`, `brand.padding`, `brand.android`, `brand.ios`, and `brand.colors`.
|
|
@@ -384,6 +392,8 @@ Button: {
|
|
|
384
392
|
- `brand` regenerates `app/assets/android/default.png` in Alloy projects, or `Resources/android/default.png` in Classic projects, so older Android splash paths still have a fallback.
|
|
385
393
|
- `cleanup-legacy` no longer removes `default.png`.
|
|
386
394
|
- Branding help and docs now spell out the difference between Android launcher icons, Android 12+ `splash_icon.png`, and legacy Android splash assets.
|
|
395
|
+
- `--notes` is explicit that `tiapp.xml` is not auto-edited and that apps with an existing custom Android theme should merge splash snippets into that theme instead of replacing it.
|
|
396
|
+
- `brand` does not try to manage older Android splash theme assets such as `background.png` or `background.9.png`; if a project still depends on them, that remains manual by design.
|
|
387
397
|
|
|
388
398
|
See the full release notes in [CHANGELOG.md](./CHANGELOG.md).
|
|
389
399
|
|
package/bin/purgetss
CHANGED
|
@@ -273,18 +273,25 @@ The recommended workflow is: put files in ${chalk.cyan('purgetss/brand/')}, then
|
|
|
273
273
|
CLI flags always win over config values.
|
|
274
274
|
|
|
275
275
|
Generates:
|
|
276
|
-
${chalk.yellow('DefaultIcon.png')}
|
|
277
|
-
${chalk.yellow('DefaultIcon-Dark.png')}
|
|
278
|
-
${chalk.yellow('iTunesConnect.png')}
|
|
276
|
+
${chalk.yellow('DefaultIcon.png')}/${chalk.yellow('DefaultIcon-ios.png')} Root icons (alpha + flattened)
|
|
277
|
+
${chalk.yellow('DefaultIcon-Dark.png')}/${chalk.yellow('DefaultIcon-Tinted.png')} iOS 18+ variants (Apple HIG specs)
|
|
278
|
+
${chalk.yellow('iTunesConnect.png')}/${chalk.yellow('MarketplaceArtwork.png')} App Store + Play Store artwork
|
|
279
279
|
${chalk.yellow('mipmap-*/ic_launcher_{foreground,background,monochrome}.png')} Android adaptive × 5
|
|
280
|
-
${chalk.yellow('mipmap-*/ic_launcher.png')}
|
|
281
|
-
${chalk.yellow('mipmap-anydpi-v26/ic_launcher.xml')}
|
|
282
|
-
${chalk.yellow('drawable-*/splash_icon.png')}
|
|
283
|
-
${chalk.yellow('app/assets/android/default.png')}
|
|
280
|
+
${chalk.yellow('mipmap-*/ic_launcher.png')} Android legacy × 5
|
|
281
|
+
${chalk.yellow('mipmap-anydpi-v26/ic_launcher.xml')} Adaptive icon binder
|
|
282
|
+
${chalk.yellow('drawable-*/splash_icon.png')} Android 12+ splash icon (with ${chalk.cyan('--splash')})
|
|
283
|
+
${chalk.yellow('app/assets/android/default.png')} Android <12 compatibility fallback
|
|
284
284
|
|
|
285
285
|
Android dark/light mode is handled by the ${chalk.yellow('monochrome')} adaptive layer
|
|
286
286
|
(Android 13+ tints it from the wallpaper + theme). No separate dark file exists.
|
|
287
287
|
|
|
288
|
+
Scope:
|
|
289
|
+
${chalk.green('brand')} targets the modern Titanium icon pipeline: iOS app icons,
|
|
290
|
+
Android adaptive icons, and optional Android 12+ splash artwork.
|
|
291
|
+
Older Android splash themes such as ${chalk.yellow('background.png')} / ${chalk.yellow('background.9.png')}
|
|
292
|
+
are outside the normal scope of this command and should be managed manually
|
|
293
|
+
if a project still depends on them.
|
|
294
|
+
|
|
288
295
|
Examples:
|
|
289
296
|
${chalk.cyan('purgetss brand')} # uses purgetss/brand/logo.svg + config
|
|
290
297
|
${chalk.cyan('purgetss brand')} logo.svg # explicit logo path
|
|
@@ -346,6 +353,13 @@ Writes directly to the project (auto-detects Alloy vs Classic):
|
|
|
346
353
|
Defaults come from the ${chalk.cyan('images:')} section in ${chalk.cyan('purgetss/config.cjs')}.
|
|
347
354
|
CLI flags always win over config values.
|
|
348
355
|
|
|
356
|
+
Sizing:
|
|
357
|
+
By default, sources are treated as 4× masters (xxxhdpi/@4x); all other
|
|
358
|
+
scales derive at 1/4, 1.5/4, 2/4, 3/4 of the source's natural pixels.
|
|
359
|
+
Pass ${chalk.cyan('--width <n>')} to pin Android mdpi (= iPhone @1x) to a specific width;
|
|
360
|
+
larger scales derive as ×1.5, ×2, ×3, ×4 (height stays proportional).
|
|
361
|
+
Recommended for SVGs from vector editors with oversized viewBoxes.
|
|
362
|
+
|
|
349
363
|
Examples:
|
|
350
364
|
${chalk.cyan('purgetss images')} # uses purgetss/images/ + config
|
|
351
365
|
${chalk.cyan('purgetss images')} ./docs/screenshots # scope to one folder
|
|
@@ -354,12 +368,15 @@ Examples:
|
|
|
354
368
|
${chalk.cyan('purgetss images')} --ios # iPhone scales only
|
|
355
369
|
${chalk.cyan('purgetss images')} --format webp # convert all outputs to WebP
|
|
356
370
|
${chalk.cyan('purgetss images')} --format png --quality 95
|
|
371
|
+
${chalk.cyan('purgetss images')} logo.svg --width 256 # pin @1x/mdpi to 256 px wide
|
|
372
|
+
${chalk.cyan('purgetss images')} banner.svg --width 512 --format webp
|
|
357
373
|
${chalk.cyan('purgetss images')} --dry-run
|
|
358
374
|
`)
|
|
359
375
|
.option('--android', 'Generate only Android density variants (skip iPhone)')
|
|
360
376
|
.option('--ios', 'Generate only iPhone scale variants (skip Android)')
|
|
361
377
|
.option('--format <ext>', 'Convert all outputs to this format: webp|jpeg|png|avif|gif|tiff')
|
|
362
378
|
.option('--quality <n>', 'JPEG/WebP/AVIF quality 0-100 (default: 85)', (v) => parseInt(v, 10))
|
|
379
|
+
.option('--width <n>', 'Target width in px for @1x/mdpi (other scales derive: ×1.5, ×2, ×3, ×4)', (v) => parseInt(v, 10))
|
|
363
380
|
.option('--project <path>', 'Project root (default: cwd)')
|
|
364
381
|
.option('--dry-run', 'Preview without writing any files')
|
|
365
382
|
.option('-y, --yes', 'Skip the overwrite confirmation prompt')
|
package/dist/purgetss.ui.js
CHANGED
|
@@ -13,15 +13,7 @@ module.exports = {
|
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
15
|
brand: {
|
|
16
|
-
logos: {
|
|
17
|
-
// Optional overrides. If omitted, PurgeTSS auto-discovers files from purgetss/brand/:
|
|
18
|
-
// primary: './docs/logo.svg',
|
|
19
|
-
// androidLauncher: './docs/app-icon.svg',
|
|
20
|
-
// androidSplash: './docs/splash.svg',
|
|
21
|
-
// monochrome: './docs/logo-mono.svg',
|
|
22
|
-
// iosDark: './docs/logo-dark.svg',
|
|
23
|
-
// iosTinted: './docs/logo-tinted.svg'
|
|
24
|
-
},
|
|
16
|
+
logos: {}, // empty = auto-discovers from purgetss/brand/
|
|
25
17
|
padding: {
|
|
26
18
|
ios: '4%', // iOS aesthetic. Range: 2% bold — 8% conservative. No launcher mask.
|
|
27
19
|
androidLegacy: '10%', // legacy ic_launcher.png padding
|
|
@@ -31,15 +23,14 @@ module.exports = {
|
|
|
31
23
|
splash: false, // also generate splash_icon.png × 5
|
|
32
24
|
notification: false // also generate ic_stat_notify.png × 5
|
|
33
25
|
},
|
|
26
|
+
ios: {
|
|
27
|
+
dark: true, // generate iOS 18+ Dark appearance icon
|
|
28
|
+
tinted: true, // generate iOS 18+ Tinted appearance icon
|
|
29
|
+
darkBackground: null // null = transparent per Apple HIG
|
|
30
|
+
},
|
|
34
31
|
colors: {
|
|
35
32
|
background: '#FFFFFF' // Android adaptive bg + iOS/marketplace flatten
|
|
36
33
|
},
|
|
37
|
-
// Optional iOS overrides:
|
|
38
|
-
// ios: {
|
|
39
|
-
// dark: false,
|
|
40
|
-
// tinted: false,
|
|
41
|
-
// darkBackground: '#111111'
|
|
42
|
-
// },
|
|
43
34
|
confirmOverwrites: true // prompt before overwriting files (set false to skip)
|
|
44
35
|
},
|
|
45
36
|
images: {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "purgetss",
|
|
4
|
-
"version": "7.
|
|
4
|
+
"version": "7.9.0",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"purgetss": "bin/purgetss"
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"dist/*.tss",
|
|
18
18
|
"dist/configs/",
|
|
19
19
|
"assets/fonts/",
|
|
20
|
-
"
|
|
20
|
+
"src/core/builders/auto-utilities-builder.js"
|
|
21
21
|
],
|
|
22
22
|
"publishConfig": {
|
|
23
23
|
"access": "public"
|
|
@@ -14,6 +14,7 @@ import { alloyProject } from '../../shared/utils.js'
|
|
|
14
14
|
import { ensureConfig } from '../../shared/config-manager.js'
|
|
15
15
|
import { buildTailwindBasedOnConfigOptions } from '../../core/builders/tailwind-builder.js'
|
|
16
16
|
import { createDefinitionsFile } from './init.js'
|
|
17
|
+
import { flushSemanticColors } from '../../shared/semantic-helpers.js'
|
|
17
18
|
|
|
18
19
|
// Import FontAwesome functions from their new modular location
|
|
19
20
|
import { buildFontAwesome, buildFontAwesomeJS } from '../../dev/builders/fontawesome-builder.js'
|
|
@@ -28,10 +29,14 @@ import { buildFontAwesome, buildFontAwesomeJS } from '../../dev/builders/fontawe
|
|
|
28
29
|
export function build(options) {
|
|
29
30
|
if (alloyProject()) {
|
|
30
31
|
ensureConfig()
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
try {
|
|
33
|
+
buildTailwindBasedOnConfigOptions(options)
|
|
34
|
+
buildFontAwesome()
|
|
35
|
+
buildFontAwesomeJS()
|
|
36
|
+
createDefinitionsFile()
|
|
37
|
+
} finally {
|
|
38
|
+
flushSemanticColors()
|
|
39
|
+
}
|
|
35
40
|
return true
|
|
36
41
|
}
|
|
37
42
|
return false
|
|
@@ -37,6 +37,13 @@ export async function images(cliSource, options = {}) {
|
|
|
37
37
|
process.exit(1)
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
if (options.width !== undefined) {
|
|
41
|
+
if (!Number.isFinite(options.width) || !Number.isInteger(options.width) || options.width < 1 || options.width > 8192) {
|
|
42
|
+
logger.error(`Invalid --width '${options.width}'. Must be an integer between 1 and 8192.`)
|
|
43
|
+
process.exit(1)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
40
47
|
const format = options.format ?? cfg.format ?? null
|
|
41
48
|
if (format && !VALID_FORMATS.has(format.toLowerCase())) {
|
|
42
49
|
logger.error(`Invalid --format '${format}'. Valid: ${[...VALID_FORMATS].join(', ')}`)
|
|
@@ -57,6 +64,7 @@ export async function images(cliSource, options = {}) {
|
|
|
57
64
|
iphoneOnly: Boolean(options.ios),
|
|
58
65
|
format: format ? format.toLowerCase() : null,
|
|
59
66
|
quality: options.quality ?? cfg.quality ?? 85,
|
|
67
|
+
baseWidth: options.width ?? null,
|
|
60
68
|
dryRun: Boolean(options.dryRun),
|
|
61
69
|
yes: Boolean(options.yes),
|
|
62
70
|
confirmOverwrites: cfg.confirmOverwrites !== false
|
|
@@ -33,6 +33,7 @@ import { getConfigOptions, getConfigFile, ensureConfig } from '../../shared/conf
|
|
|
33
33
|
// Import purger functions from core modules
|
|
34
34
|
import { processControllers } from '../../core/analyzers/class-extractor.js'
|
|
35
35
|
import { purgeTailwind } from '../../core/purger/tailwind-purger.js'
|
|
36
|
+
import { flushSemanticColors } from '../../shared/semantic-helpers.js'
|
|
36
37
|
import {
|
|
37
38
|
purgeFontAwesome,
|
|
38
39
|
purgeMaterialIcons,
|
|
@@ -40,6 +41,7 @@ import {
|
|
|
40
41
|
purgeFramework7
|
|
41
42
|
} from '../../core/purger/icon-purger.js'
|
|
42
43
|
import { purgeFonts } from '../../core/purger/fonts-purger.js'
|
|
44
|
+
import { validateClassSyntax } from '../utils/unsupported-class-reporter.js'
|
|
43
45
|
|
|
44
46
|
// Global variables (EXACT copies from original src/index.js)
|
|
45
47
|
let configOptions = {}
|
|
@@ -693,10 +695,21 @@ export function purgeClasses(options) {
|
|
|
693
695
|
|
|
694
696
|
try {
|
|
695
697
|
uniqueClasses = getUniqueClasses()
|
|
698
|
+
|
|
699
|
+
// Pre-validate class syntax. Halts on inverted negatives, Tailwind
|
|
700
|
+
// brackets, empty parens, etc. — but NOT on generic unknown classes
|
|
701
|
+
// (those fall through silently to "// Unused or unsupported classes").
|
|
702
|
+
validateClassSyntax({
|
|
703
|
+
classes: uniqueClasses,
|
|
704
|
+
viewPaths: getViewPaths(),
|
|
705
|
+
controllerPaths: getControllerPaths()
|
|
706
|
+
})
|
|
696
707
|
} catch (error) {
|
|
697
|
-
// Handle pre-validation errors (XML syntax errors detected before
|
|
698
|
-
|
|
699
|
-
|
|
708
|
+
// Handle pre-validation errors (XML syntax errors detected before
|
|
709
|
+
// parsing) and class-syntax errors (authoring mistakes detected
|
|
710
|
+
// before purging). In both cases the error block was already
|
|
711
|
+
// printed; just exit cleanly.
|
|
712
|
+
if (error.isPreValidationError || error.isClassSyntaxError) {
|
|
700
713
|
// eslint-disable-next-line n/no-process-exit
|
|
701
714
|
process.exit(1)
|
|
702
715
|
}
|
|
@@ -728,6 +741,7 @@ export function purgeClasses(options) {
|
|
|
728
741
|
|
|
729
742
|
finish()
|
|
730
743
|
} finally {
|
|
744
|
+
flushSemanticColors()
|
|
731
745
|
logger.endSection()
|
|
732
746
|
}
|
|
733
747
|
|
|
@@ -329,8 +329,8 @@ export function buildSemanticPalette(family, kebabName, alpha) {
|
|
|
329
329
|
const mirror = sorted[sorted.length - 1 - i]
|
|
330
330
|
const key = `${camelName}${shade.number}`
|
|
331
331
|
semanticEntries[key] = {
|
|
332
|
-
light: wrapValue(
|
|
333
|
-
dark: wrapValue(
|
|
332
|
+
light: wrapValue(shade.hexcode, alpha),
|
|
333
|
+
dark: wrapValue(mirror.hexcode, alpha)
|
|
334
334
|
}
|
|
335
335
|
configMapping[shade.number] = key
|
|
336
336
|
})
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PurgeTSS - Class Syntax Validator
|
|
3
|
+
*
|
|
4
|
+
* Pre-validation helper that runs BEFORE the purge starts. Scans every unique
|
|
5
|
+
* class name pulled from XML views (and JS controllers) for well-known
|
|
6
|
+
* authoring mistakes:
|
|
7
|
+
*
|
|
8
|
+
* - Inverted negative sign: top-(-10) → -top-(10)
|
|
9
|
+
* - Tailwind-style brackets: top-[10px] → top-(10px)
|
|
10
|
+
* - Empty parentheses: wh-() → wh-(<value>)
|
|
11
|
+
* - Whitespace in parens: wh-( 200 ) → wh-(200)
|
|
12
|
+
* - Redundant px units: top-(10px) → top-(10)
|
|
13
|
+
*
|
|
14
|
+
* On any match the validator collects every offender, prints one block per
|
|
15
|
+
* offender (file + line + fix, mirroring throwPreValidationError) and throws
|
|
16
|
+
* an `isClassSyntaxError` so the CLI exits cleanly without running the purge.
|
|
17
|
+
*
|
|
18
|
+
* Generic unknown classes (typos, vendor utilities not enabled, custom
|
|
19
|
+
* classes not yet defined) are intentionally left alone — they are still
|
|
20
|
+
* captured silently in the "// Unused or unsupported classes" section of
|
|
21
|
+
* app.tss, but never trigger warnings or halts. That's the only way to keep
|
|
22
|
+
* the workflow usable on projects with dozens of in-progress class names.
|
|
23
|
+
*
|
|
24
|
+
* @author César Estrada
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import fs from 'fs'
|
|
28
|
+
import chalk from 'chalk'
|
|
29
|
+
import { logger } from '../../shared/logger.js'
|
|
30
|
+
|
|
31
|
+
const cwd = process.cwd()
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Pattern detectors. Each detector receives the className and returns
|
|
35
|
+
* { issue, suggestion } when it recognises the problem, or null otherwise.
|
|
36
|
+
* Order matters: more specific detectors first.
|
|
37
|
+
*/
|
|
38
|
+
const detectors = [
|
|
39
|
+
// top-(-10) → -top-(10)
|
|
40
|
+
function detectInvertedNegative(className) {
|
|
41
|
+
const m = className.match(/^([a-zA-Z][\w-]*?)-\(-([^()]+)\)$/)
|
|
42
|
+
if (!m) return null
|
|
43
|
+
return {
|
|
44
|
+
issue: 'Negative sign is inside the parentheses',
|
|
45
|
+
suggestion: `Use ${chalk.green(`"-${m[1]}-(${m[2]})"`)} — PurgeTSS expects the "-" prefix BEFORE the rule, not inside the value`
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
// top-[10px] → top-(10)
|
|
50
|
+
function detectTailwindBrackets(className) {
|
|
51
|
+
if (!className.includes('[') && !className.includes(']')) return null
|
|
52
|
+
const fixed = className.replace(/\[/g, '(').replace(/\]/g, ')')
|
|
53
|
+
return {
|
|
54
|
+
issue: 'Tailwind-style brackets "[ ]" are not supported',
|
|
55
|
+
suggestion: `Use parentheses instead: ${chalk.green(`"${fixed}"`)}`
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
// wh-() → "Add a value"
|
|
60
|
+
function detectEmptyParens(className) {
|
|
61
|
+
const m = className.match(/^([\w-]+)-\(\s*\)$/)
|
|
62
|
+
if (!m) return null
|
|
63
|
+
return {
|
|
64
|
+
issue: 'Empty value inside parentheses',
|
|
65
|
+
suggestion: `Add a value, e.g. ${chalk.green(`"${m[1]}-(10)"`)}`
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
// wh-( 200 ) → wh-(200)
|
|
70
|
+
function detectSpacesInParens(className) {
|
|
71
|
+
const m = className.match(/^([\w-]+)-\(\s+([^()]*?)\s*\)$|^([\w-]+)-\(([^()]*?)\s+\)$/)
|
|
72
|
+
if (!m) return null
|
|
73
|
+
const rule = m[1] || m[3]
|
|
74
|
+
const value = (m[2] || m[4] || '').trim()
|
|
75
|
+
if (!value) return null
|
|
76
|
+
return {
|
|
77
|
+
issue: 'Whitespace inside parentheses',
|
|
78
|
+
suggestion: `Remove the spaces: ${chalk.green(`"${rule}-(${value})"`)}`
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
// top-(10px) — units inside parens that PurgeTSS would strip anyway.
|
|
83
|
+
// Only flag when value is purely numeric+unit; never blanket-flag because
|
|
84
|
+
// some properties (durations, percentages) accept units.
|
|
85
|
+
function detectRedundantUnits(className) {
|
|
86
|
+
const m = className.match(/^([\w-]+)-\((-?\d+(?:\.\d+)?)px\)$/)
|
|
87
|
+
if (!m) return null
|
|
88
|
+
return {
|
|
89
|
+
issue: 'Explicit "px" unit is redundant',
|
|
90
|
+
suggestion: `PurgeTSS treats unit-less values as pixels: ${chalk.green(`"${m[1]}-(${m[2]})"`)}`
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
function detectIssue(className) {
|
|
96
|
+
for (const detector of detectors) {
|
|
97
|
+
const result = detector(className)
|
|
98
|
+
if (result) return result
|
|
99
|
+
}
|
|
100
|
+
return null
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Find every (file, line) where `className` appears as a whole token inside a
|
|
105
|
+
* class="..." attribute. Uses an attribute-then-split strategy (rather than
|
|
106
|
+
* matching the class name directly) so arbitrary-value class names like
|
|
107
|
+
* "top-(-10)" — which contain regex-special characters — do not need escaping
|
|
108
|
+
* and cannot false-match a partial substring.
|
|
109
|
+
*/
|
|
110
|
+
function findLocations(className, paths) {
|
|
111
|
+
const locations = []
|
|
112
|
+
const attrRe = /class\s*=\s*["']([^"']*)["']/g
|
|
113
|
+
|
|
114
|
+
for (const filePath of paths) {
|
|
115
|
+
let content
|
|
116
|
+
try {
|
|
117
|
+
content = fs.readFileSync(filePath, 'utf8')
|
|
118
|
+
} catch {
|
|
119
|
+
continue
|
|
120
|
+
}
|
|
121
|
+
const lines = content.split(/\r?\n/)
|
|
122
|
+
for (let i = 0; i < lines.length; i++) {
|
|
123
|
+
attrRe.lastIndex = 0
|
|
124
|
+
let match
|
|
125
|
+
while ((match = attrRe.exec(lines[i])) !== null) {
|
|
126
|
+
const tokens = match[1].split(/\s+/).filter(Boolean)
|
|
127
|
+
if (tokens.includes(className)) {
|
|
128
|
+
locations.push({
|
|
129
|
+
filePath,
|
|
130
|
+
lineNumber: i + 1,
|
|
131
|
+
lineContent: lines[i].trim()
|
|
132
|
+
})
|
|
133
|
+
break
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return locations
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function relativePath(filePath) {
|
|
142
|
+
return filePath.startsWith(cwd + '/') ? filePath.slice(cwd.length + 1) : filePath
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Pre-validate every unique class against the syntax detectors. Collects all
|
|
147
|
+
* offenders (so the dev sees the full list in a single run instead of
|
|
148
|
+
* one-fix-per-attempt), prints a block per offender, then throws.
|
|
149
|
+
*
|
|
150
|
+
* Generic unknown classes are NOT flagged — they fall through silently to
|
|
151
|
+
* the "// Unused or unsupported classes" comment block in app.tss.
|
|
152
|
+
*
|
|
153
|
+
* @param {Object} args
|
|
154
|
+
* @param {string[]} args.classes Unique classes pulled from views/controllers.
|
|
155
|
+
* @param {string[]} args.viewPaths Absolute paths to XML view files.
|
|
156
|
+
* @param {string[]} [args.controllerPaths] Absolute paths to JS controller files.
|
|
157
|
+
* @throws {Error} with `isClassSyntaxError === true` if any class matches a detector.
|
|
158
|
+
*/
|
|
159
|
+
export function validateClassSyntax({ classes, viewPaths, controllerPaths = [] }) {
|
|
160
|
+
if (!classes || classes.length === 0) return
|
|
161
|
+
|
|
162
|
+
const allPaths = [...viewPaths, ...controllerPaths]
|
|
163
|
+
const offenders = []
|
|
164
|
+
|
|
165
|
+
for (const className of classes) {
|
|
166
|
+
const issue = detectIssue(className)
|
|
167
|
+
if (!issue) continue
|
|
168
|
+
offenders.push({
|
|
169
|
+
className,
|
|
170
|
+
issue,
|
|
171
|
+
locations: findLocations(className, allPaths)
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (offenders.length === 0) return
|
|
176
|
+
|
|
177
|
+
for (const offender of offenders) {
|
|
178
|
+
const lines = []
|
|
179
|
+
lines.push(`Class: ${chalk.yellow(`"${offender.className}"`)}`)
|
|
180
|
+
|
|
181
|
+
if (offender.locations.length > 0) {
|
|
182
|
+
const first = offender.locations[0]
|
|
183
|
+
lines.push(`File: ${chalk.yellow(`"${relativePath(first.filePath)}"`)}`)
|
|
184
|
+
lines.push(`Line: ${chalk.yellow(first.lineNumber)}`)
|
|
185
|
+
lines.push(`Content: ${chalk.gray(first.lineContent)}`)
|
|
186
|
+
if (offender.locations.length > 1) {
|
|
187
|
+
const more = offender.locations.length - 1
|
|
188
|
+
lines.push(chalk.gray(`(also used in ${more} other location${more === 1 ? '' : 's'})`))
|
|
189
|
+
}
|
|
190
|
+
} else {
|
|
191
|
+
lines.push(chalk.gray('Location: not found in views — likely from a controller or safelist'))
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
lines.push('')
|
|
195
|
+
lines.push(chalk.red(`Issue: ${offender.issue.issue}`))
|
|
196
|
+
lines.push(`${chalk.green('Fix:')} ${offender.issue.suggestion}`)
|
|
197
|
+
|
|
198
|
+
logger.block(chalk.red('Class Syntax Error'), ...lines)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const word = offenders.length === 1 ? 'class syntax error' : 'class syntax errors'
|
|
202
|
+
logger.block(
|
|
203
|
+
chalk.red(`Found ${offenders.length} ${word} — fix the ${offenders.length === 1 ? 'class' : 'classes'} above and re-run purgetss`)
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
const error = new Error('Class syntax errors detected')
|
|
207
|
+
error.isClassSyntaxError = true
|
|
208
|
+
throw error
|
|
209
|
+
}
|
|
@@ -23,15 +23,7 @@ import { projectsConfigJS, projectsPurge_TSS_Brand_Folder } from '../../shared/c
|
|
|
23
23
|
import { logger } from './branding-logger.js'
|
|
24
24
|
|
|
25
25
|
const BRAND_BLOCK = ` brand: {
|
|
26
|
-
logos: {
|
|
27
|
-
// Optional overrides. If omitted, PurgeTSS auto-discovers files from purgetss/brand/:
|
|
28
|
-
// primary: './docs/logo.svg',
|
|
29
|
-
// androidLauncher: './docs/app-icon.svg',
|
|
30
|
-
// androidSplash: './docs/splash.svg',
|
|
31
|
-
// monochrome: './docs/logo-mono.svg',
|
|
32
|
-
// iosDark: './docs/logo-dark.svg',
|
|
33
|
-
// iosTinted: './docs/logo-tinted.svg'
|
|
34
|
-
},
|
|
26
|
+
logos: {}, // empty = auto-discovers from purgetss/brand/
|
|
35
27
|
padding: {
|
|
36
28
|
ios: '4%', // iOS aesthetic. Range: 2% bold — 8% conservative. No launcher mask.
|
|
37
29
|
androidLegacy: '10%', // legacy ic_launcher.png padding
|
|
@@ -41,15 +33,14 @@ const BRAND_BLOCK = ` brand: {
|
|
|
41
33
|
splash: false, // also generate splash_icon.png × 5
|
|
42
34
|
notification: false // also generate ic_stat_notify.png × 5
|
|
43
35
|
},
|
|
36
|
+
ios: {
|
|
37
|
+
dark: true, // generate iOS 18+ Dark appearance icon
|
|
38
|
+
tinted: true, // generate iOS 18+ Tinted appearance icon
|
|
39
|
+
darkBackground: null // null = transparent per Apple HIG
|
|
40
|
+
},
|
|
44
41
|
colors: {
|
|
45
42
|
background: '#FFFFFF' // Android adaptive bg + iOS/marketplace flatten
|
|
46
43
|
},
|
|
47
|
-
// Optional iOS overrides:
|
|
48
|
-
// ios: {
|
|
49
|
-
// dark: false,
|
|
50
|
-
// tinted: false,
|
|
51
|
-
// darkBackground: '#111111'
|
|
52
|
-
// },
|
|
53
44
|
confirmOverwrites: true // prompt before overwriting files (set false to skip)
|
|
54
45
|
},
|
|
55
46
|
`
|
|
@@ -84,6 +84,10 @@ function printFullNotes(opts) {
|
|
|
84
84
|
logger.section('Configuration reminders')
|
|
85
85
|
console.log(' The tool does NOT auto-edit tiapp.xml. Snippets below are optional —')
|
|
86
86
|
console.log(' paste only what you need, after reviewing.')
|
|
87
|
+
console.log(' If your app already uses a custom Android theme, merge these changes')
|
|
88
|
+
console.log(' into that theme instead of replacing it blindly.')
|
|
89
|
+
console.log(' brand is designed around the modern Titanium icon pipeline, not around')
|
|
90
|
+
console.log(' older Android splash themes such as background.png / background.9.png.')
|
|
87
91
|
console.log()
|
|
88
92
|
console.log(` ${chalk.yellow('⚠')} ${chalk.yellow('tiapp.xml <application> tag may be self-closing')}`)
|
|
89
93
|
console.log(' If yours looks like:')
|
|
@@ -114,14 +118,18 @@ function printFullNotes(opts) {
|
|
|
114
118
|
console.log(' If you want the Android 12+ splash to use splash_icon instead of')
|
|
115
119
|
console.log(' ic_launcher, add a custom theme and point')
|
|
116
120
|
console.log(code(' <item name="android:windowSplashScreenAnimatedIcon">@drawable/splash_icon</item>'))
|
|
121
|
+
console.log(' If you still see a brief flash during splash exit, the artifact may')
|
|
122
|
+
console.log(' come from Titanium or the system splash transition rather than from')
|
|
123
|
+
console.log(' the generated PNG assets themselves.')
|
|
117
124
|
}
|
|
118
125
|
|
|
119
126
|
console.log()
|
|
120
127
|
console.log(` ${num(withSplash ? '4.' : '3.')} ${chalk.cyan('Android <12 legacy splash')}`)
|
|
121
|
-
console.log(' PurgeTSS brand
|
|
122
|
-
console.log(' a
|
|
123
|
-
console.log(' If your app uses a custom
|
|
124
|
-
console.log(' that custom theme still
|
|
128
|
+
console.log(' PurgeTSS brand still regenerates app/assets/android/default.png as')
|
|
129
|
+
console.log(' a compatibility fallback while Titanium continues to recognize it.')
|
|
130
|
+
console.log(' It is not the primary modern path. If your app uses a custom')
|
|
131
|
+
console.log(' windowBackground / background.9.png theme, that custom theme still')
|
|
132
|
+
console.log(' takes precedence and should be managed manually.')
|
|
125
133
|
|
|
126
134
|
if (withNotification) {
|
|
127
135
|
const colorsDir = projectType === 'classic'
|