purgetss 7.5.1 → 7.5.3
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 +61 -0
- package/dist/purgetss.ui.js +43 -1
- package/dist/utilities.tss +8 -3
- package/experimental/completions2.js +1 -1
- package/lib/templates/purgetss.ui.js.cjs +42 -0
- package/package.json +1 -1
- package/src/cli/commands/purge.js +201 -35
- package/src/core/builders/tailwind-helpers.js +1 -1
- package/src/shared/helpers/typography.js +38 -3
package/README.md
CHANGED
|
@@ -240,6 +240,67 @@ Fallback defaults when not set: `swap`/`reorder`/`snapTo` → 200ms; `shake` →
|
|
|
240
240
|
|
|
241
241
|
See the full documentation at [purgetss.com/docs/animation-module/introduction](https://purgetss.com/docs/animation-module/introduction).
|
|
242
242
|
|
|
243
|
+
### Appearance management
|
|
244
|
+
|
|
245
|
+
Switch between Light, Dark, and System modes with automatic persistence:
|
|
246
|
+
|
|
247
|
+
```js
|
|
248
|
+
const { Appearance } = require('purgetss.ui')
|
|
249
|
+
|
|
250
|
+
// Call once at app startup (e.g., in index.js before opening the first window)
|
|
251
|
+
Appearance.init()
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
| Method | Description |
|
|
255
|
+
| ----------- | ------------------------------------------------------------ |
|
|
256
|
+
| `init()` | Restore the saved mode from `Ti.App.Properties` |
|
|
257
|
+
| `get()` | Returns the current mode string |
|
|
258
|
+
| `set(mode)` | Apply and persist a mode: `'system'`, `'light'`, or `'dark'` |
|
|
259
|
+
| `toggle()` | Switch between `'light'` and `'dark'` |
|
|
260
|
+
|
|
261
|
+
Use it from any controller to respond to user actions:
|
|
262
|
+
|
|
263
|
+
```js
|
|
264
|
+
const { Appearance } = require('purgetss.ui')
|
|
265
|
+
|
|
266
|
+
function selectDark() { Appearance.set('dark') }
|
|
267
|
+
function selectLight() { Appearance.set('light') }
|
|
268
|
+
function selectSystem() { Appearance.set('system') }
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Requires `semantic.colors.json` in `app/assets/` for views to respond to mode changes. See the [Titanium docs on semantic colors](https://titaniumsdk.com/guide/Titanium_SDK/Titanium_SDK_How-tos/User_Interface_Deep_Dives/iOS_Dark_Mode.html) for the file format.
|
|
272
|
+
|
|
273
|
+
### Default font families
|
|
274
|
+
|
|
275
|
+
PurgeTSS generates `font-sans`, `font-serif`, and `font-mono` classes automatically with platform-appropriate values:
|
|
276
|
+
|
|
277
|
+
| Class | iOS | Android |
|
|
278
|
+
| ------------ | ------------------ | ------------- |
|
|
279
|
+
| `font-sans` | `Helvetica Neue` | `sans-serif` |
|
|
280
|
+
| `font-serif` | `Georgia` | `serif` |
|
|
281
|
+
| `font-mono` | `monospace` | `monospace` |
|
|
282
|
+
|
|
283
|
+
Override or add families in `config.cjs`:
|
|
284
|
+
|
|
285
|
+
```js
|
|
286
|
+
// theme.extend.fontFamily → adds to defaults
|
|
287
|
+
extend: {
|
|
288
|
+
fontFamily: {
|
|
289
|
+
display: 'AlfaSlabOne-Regular',
|
|
290
|
+
body: 'BarlowSemiCondensed-Regular'
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// theme.fontFamily → replaces defaults entirely
|
|
295
|
+
theme: {
|
|
296
|
+
fontFamily: {
|
|
297
|
+
sans: 'System',
|
|
298
|
+
mono: 'Courier',
|
|
299
|
+
display: 'AlfaSlabOne-Regular'
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
243
304
|
---
|
|
244
305
|
|
|
245
306
|
## Customizing default components
|
package/dist/purgetss.ui.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// PurgeTSS v7.5.
|
|
1
|
+
// PurgeTSS v7.5.3
|
|
2
2
|
// Created by César Estrada
|
|
3
3
|
// https://purgetss.com
|
|
4
4
|
|
|
@@ -906,3 +906,45 @@ function saveComponent({ source, directory = Ti.Filesystem.tempDirectory }) {
|
|
|
906
906
|
exports.saveComponent = saveComponent
|
|
907
907
|
|
|
908
908
|
exports.createAnimation = (args) => new Animation(args)
|
|
909
|
+
|
|
910
|
+
// --- Appearance Management (Light/Dark/System) ---
|
|
911
|
+
function Appearance() {
|
|
912
|
+
const PROP_KEY = 'userInterfaceStyle'
|
|
913
|
+
const STYLES = {
|
|
914
|
+
dark: Ti.UI.USER_INTERFACE_STYLE_DARK,
|
|
915
|
+
light: Ti.UI.USER_INTERFACE_STYLE_LIGHT,
|
|
916
|
+
system: Ti.UI.USER_INTERFACE_STYLE_UNSPECIFIED
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
let currentMode = 'system'
|
|
920
|
+
|
|
921
|
+
function applyMode(mode) {
|
|
922
|
+
currentMode = mode
|
|
923
|
+
Ti.UI.overrideUserInterfaceStyle = STYLES[mode]
|
|
924
|
+
Ti.App.Properties.setInt(PROP_KEY, STYLES[mode])
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
return {
|
|
928
|
+
init() {
|
|
929
|
+
const saved = Ti.App.Properties.getInt(PROP_KEY, STYLES.system)
|
|
930
|
+
currentMode = Object.keys(STYLES).find(key => STYLES[key] === saved) || 'system'
|
|
931
|
+
Ti.UI.overrideUserInterfaceStyle = saved
|
|
932
|
+
},
|
|
933
|
+
|
|
934
|
+
set(mode) {
|
|
935
|
+
if (!STYLES.hasOwnProperty(mode)) return
|
|
936
|
+
applyMode(mode)
|
|
937
|
+
},
|
|
938
|
+
|
|
939
|
+
get() {
|
|
940
|
+
return currentMode
|
|
941
|
+
},
|
|
942
|
+
|
|
943
|
+
toggle() {
|
|
944
|
+
const next = (currentMode === 'dark') ? 'light' : 'dark'
|
|
945
|
+
applyMode(next)
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
exports.Appearance = Appearance()
|
package/dist/utilities.tss
CHANGED
|
@@ -2250,9 +2250,6 @@
|
|
|
2250
2250
|
'.rounded-full-2.5': { width: 10, height: 10, borderRadius: 5 }
|
|
2251
2251
|
'.rounded-full-3.5': { width: 14, height: 14, borderRadius: 7 }
|
|
2252
2252
|
|
|
2253
|
-
// Property(ies): fontFamily
|
|
2254
|
-
// Component(s): Ti.UI.ActivityIndicator, Ti.UI.Button, Ti.UI.Label, Ti.UI.ListItem, Ti.UI.Picker, Ti.UI.PickerColumn, Ti.UI.PickerRow, Ti.UI.ProgressBar, Ti.UI.Switch, Ti.UI.TableViewRow, Ti.UI.TextArea, Ti.UI.TextField
|
|
2255
|
-
|
|
2256
2253
|
// Property(ies): fontSize
|
|
2257
2254
|
// Component(s): Ti.UI.ActivityIndicator, Ti.UI.Button, Ti.UI.Label, Ti.UI.ListItem, Ti.UI.Picker, Ti.UI.PickerColumn, Ti.UI.PickerRow, Ti.UI.ProgressBar, Ti.UI.Switch, Ti.UI.TableViewRow, Ti.UI.TextArea, Ti.UI.TextField
|
|
2258
2255
|
'.text-xs': { font: { fontSize: 12 } }
|
|
@@ -2281,6 +2278,14 @@
|
|
|
2281
2278
|
'.font-extrabold': { font: { fontWeight: 'bold' } }
|
|
2282
2279
|
'.font-black': { font: { fontWeight: 'bold' } }
|
|
2283
2280
|
|
|
2281
|
+
// Property(ies): fontFamily
|
|
2282
|
+
// Component(s): Ti.UI.ActivityIndicator, Ti.UI.Button, Ti.UI.Label, Ti.UI.ListItem, Ti.UI.Picker, Ti.UI.PickerColumn, Ti.UI.PickerRow, Ti.UI.ProgressBar, Ti.UI.Switch, Ti.UI.TableViewRow, Ti.UI.TextArea, Ti.UI.TextField
|
|
2283
|
+
'.font-mono': { font: { fontFamily: 'monospace' } }
|
|
2284
|
+
'.font-sans[platform=ios]': { font: { fontFamily: 'Helvetica Neue' } }
|
|
2285
|
+
'.font-serif[platform=ios]': { font: { fontFamily: 'Georgia' } }
|
|
2286
|
+
'.font-sans[platform=android]': { font: { fontFamily: 'sans-serif' } }
|
|
2287
|
+
'.font-serif[platform=android]': { font: { fontFamily: 'serif' } }
|
|
2288
|
+
|
|
2284
2289
|
// Property(ies): top, right, bottom, left - Gap for Grid System
|
|
2285
2290
|
// Component(s): Ti.UI.ActivityIndicator, Ti.UI.Animation, Ti.UI.View, Ti.UI.Window
|
|
2286
2291
|
'.gap-0': { top: 0, right: 0, bottom: 0, left: 0 }
|
|
@@ -247,9 +247,9 @@ function processCompoundClasses({ ..._base }) {
|
|
|
247
247
|
// ! Configurables
|
|
248
248
|
compoundClasses += generateGlossary('borderRadius-alternative', helpers.borderRadius(_base.borderRadius))
|
|
249
249
|
compoundClasses += generateGlossary('borderRadius-full', helpers.borderRadiusFull(_base.borderRadius))
|
|
250
|
-
compoundClasses += generateGlossary('fontFamily', helpers.fontFamily(_base.fontFamily))
|
|
251
250
|
compoundClasses += generateGlossary('fontSize', helpers.fontSize(_base.fontSize))
|
|
252
251
|
compoundClasses += generateGlossary('fontWeight', helpers.fontWeight(_base.fontWeight))
|
|
252
|
+
compoundClasses += generateGlossary('fontFamily', helpers.fontFamily(_base.fontFamily))
|
|
253
253
|
compoundClasses += generateGlossary('margin-alternative', helpers.gap(_base.margin))
|
|
254
254
|
compoundClasses += generateGlossary('minimumFontSize', helpers.minimumFontSize(_base.fontSize))
|
|
255
255
|
compoundClasses += generateGlossary('padding-alternative', helpers.padding(_base.padding))
|
|
@@ -905,3 +905,45 @@ function saveComponent({ source, directory = Ti.Filesystem.tempDirectory }) {
|
|
|
905
905
|
exports.saveComponent = saveComponent
|
|
906
906
|
|
|
907
907
|
exports.createAnimation = (args) => new Animation(args)
|
|
908
|
+
|
|
909
|
+
// --- Appearance Management (Light/Dark/System) ---
|
|
910
|
+
function Appearance() {
|
|
911
|
+
const PROP_KEY = 'userInterfaceStyle'
|
|
912
|
+
const STYLES = {
|
|
913
|
+
dark: Ti.UI.USER_INTERFACE_STYLE_DARK,
|
|
914
|
+
light: Ti.UI.USER_INTERFACE_STYLE_LIGHT,
|
|
915
|
+
system: Ti.UI.USER_INTERFACE_STYLE_UNSPECIFIED
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
let currentMode = 'system'
|
|
919
|
+
|
|
920
|
+
function applyMode(mode) {
|
|
921
|
+
currentMode = mode
|
|
922
|
+
Ti.UI.overrideUserInterfaceStyle = STYLES[mode]
|
|
923
|
+
Ti.App.Properties.setInt(PROP_KEY, STYLES[mode])
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
return {
|
|
927
|
+
init() {
|
|
928
|
+
const saved = Ti.App.Properties.getInt(PROP_KEY, STYLES.system)
|
|
929
|
+
currentMode = Object.keys(STYLES).find(key => STYLES[key] === saved) || 'system'
|
|
930
|
+
Ti.UI.overrideUserInterfaceStyle = saved
|
|
931
|
+
},
|
|
932
|
+
|
|
933
|
+
set(mode) {
|
|
934
|
+
if (!STYLES.hasOwnProperty(mode)) return
|
|
935
|
+
applyMode(mode)
|
|
936
|
+
},
|
|
937
|
+
|
|
938
|
+
get() {
|
|
939
|
+
return currentMode
|
|
940
|
+
},
|
|
941
|
+
|
|
942
|
+
toggle() {
|
|
943
|
+
const next = (currentMode === 'dark') ? 'light' : 'dark'
|
|
944
|
+
applyMode(next)
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
exports.Appearance = Appearance()
|
package/package.json
CHANGED
|
@@ -144,25 +144,30 @@ function validateXML(xmlText, filePath) {
|
|
|
144
144
|
const columnMatch = error.message.match(/Column:\s*(\d+)/)
|
|
145
145
|
const charMatch = error.message.match(/Char:\s*(.+)/)
|
|
146
146
|
|
|
147
|
+
const lines = xmlText.split('\n')
|
|
148
|
+
const parserLine = lineMatch ? parseInt(lineMatch[1]) : null
|
|
149
|
+
|
|
150
|
+
// Try to find the real source of the error by scanning for suspicious tags.
|
|
151
|
+
// xml2json often reports errors far from the actual problem (e.g. at EOF)
|
|
152
|
+
// because it only notices the mismatch when nesting fails to close.
|
|
153
|
+
const suspectLine = findSuspectLine(lines)
|
|
154
|
+
|
|
155
|
+
const reportLine = suspectLine || parserLine
|
|
156
|
+
|
|
147
157
|
let errorMessage = chalk.red(`\n::PurgeTSS:: XML Syntax Error\n`) +
|
|
148
158
|
chalk.yellow(`File: "${filePath}"\n`)
|
|
149
159
|
|
|
150
|
-
if (
|
|
151
|
-
|
|
152
|
-
const colNum = columnMatch ? parseInt(columnMatch[1]) : '?'
|
|
153
|
-
const badChar = charMatch ? charMatch[1] : '?'
|
|
154
|
-
|
|
155
|
-
errorMessage += chalk.yellow(`Error near line: ${lineNum}\n\n`)
|
|
160
|
+
if (reportLine) {
|
|
161
|
+
errorMessage += chalk.yellow(`Error near line: ${reportLine}\n\n`)
|
|
156
162
|
|
|
157
163
|
// Extract and show context: line before, error line, and line after
|
|
158
|
-
const
|
|
159
|
-
const
|
|
160
|
-
const endLine = Math.min(lines.length, lineNum + 2)
|
|
164
|
+
const startLine = Math.max(0, reportLine - 2)
|
|
165
|
+
const endLine = Math.min(lines.length, reportLine + 2)
|
|
161
166
|
|
|
162
167
|
errorMessage += chalk.gray('Context:\n')
|
|
163
168
|
for (let i = startLine; i < endLine; i++) {
|
|
164
169
|
const lineNumDisplay = i + 1
|
|
165
|
-
const isTargetLine = (lineNumDisplay ===
|
|
170
|
+
const isTargetLine = (lineNumDisplay === reportLine)
|
|
166
171
|
const prefix = isTargetLine ? chalk.red('>>>') : chalk.gray(' ')
|
|
167
172
|
const lineContent = lines[i] || ''
|
|
168
173
|
|
|
@@ -181,54 +186,215 @@ function validateXML(xmlText, filePath) {
|
|
|
181
186
|
}
|
|
182
187
|
}
|
|
183
188
|
|
|
189
|
+
// Scan XML lines for common malformations that xml2json can't pinpoint.
|
|
190
|
+
// Returns the 1-based line number of the first suspect, or null.
|
|
191
|
+
function findSuspectLine(lines) {
|
|
192
|
+
for (let i = 0; i < lines.length; i++) {
|
|
193
|
+
const trimmed = lines[i].trim()
|
|
194
|
+
if (!trimmed) continue
|
|
195
|
+
|
|
196
|
+
// "--" inside XML comments (illegal in XML spec)
|
|
197
|
+
if (trimmed.includes('<!--')) {
|
|
198
|
+
const commentBody = trimmed.replace(/<!--/, '').replace(/-->.*$/, '')
|
|
199
|
+
if (/--/.test(commentBody)) return i + 1
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (trimmed.startsWith('<!--')) continue
|
|
203
|
+
|
|
204
|
+
// Opening tag without tag name: "< class=..."
|
|
205
|
+
if (/^<\s+\w+=/.test(trimmed)) return i + 1
|
|
206
|
+
|
|
207
|
+
// Double slash in closing tag: "<//View>"
|
|
208
|
+
if (/^<\/\/\w+>/.test(trimmed)) return i + 1
|
|
209
|
+
|
|
210
|
+
// Closing tag with extra characters: "</View/>" or "</View >>" etc.
|
|
211
|
+
if (/^<\/[a-zA-Z0-9_]+[^>]+>/.test(trimmed) && !/^<\/[a-zA-Z0-9_:]+\s*>/.test(trimmed)) return i + 1
|
|
212
|
+
|
|
213
|
+
// Opening < immediately followed by non-alpha, non-slash, non-! (e.g. "< >" or "<=>")
|
|
214
|
+
if (/^<[^a-zA-Z/!?\s]/.test(trimmed)) return i + 1
|
|
215
|
+
|
|
216
|
+
// Space between < and tag name: "< View"
|
|
217
|
+
if (/^<\s+[A-Z]/.test(trimmed)) return i + 1
|
|
218
|
+
|
|
219
|
+
// Backslash instead of forward slash: \>
|
|
220
|
+
if (/\\>/.test(trimmed)) return i + 1
|
|
221
|
+
|
|
222
|
+
// Double closing bracket: >>
|
|
223
|
+
if (/>>/.test(trimmed)) return i + 1
|
|
224
|
+
|
|
225
|
+
// Unclosed tag: starts with < but doesn't end with > and next line starts a new tag
|
|
226
|
+
if (trimmed.startsWith('<') && !trimmed.startsWith('</') && !trimmed.endsWith('>')) {
|
|
227
|
+
const nextTrimmed = (lines[i + 1] || '').trim()
|
|
228
|
+
if (nextTrimmed.startsWith('<')) return i + 1
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return null
|
|
232
|
+
}
|
|
233
|
+
|
|
184
234
|
/**
|
|
185
235
|
* Pre-validate XML for common Alloy malformations
|
|
186
236
|
* Returns Error if problem found, null if OK
|
|
187
237
|
*/
|
|
188
238
|
function preValidateXML(xmlText, filePath) {
|
|
189
239
|
const lines = xmlText.split('\n')
|
|
240
|
+
const relativePath = filePath.replace(process.cwd() + '/', '')
|
|
190
241
|
|
|
191
|
-
// Check for tags without opening < (common mistake: Label, View, etc. without <)
|
|
192
242
|
for (let i = 0; i < lines.length; i++) {
|
|
193
243
|
const line = lines[i]
|
|
194
244
|
const trimmed = line.trim()
|
|
195
245
|
|
|
196
|
-
// Skip empty lines
|
|
197
|
-
if (!trimmed || trimmed.startsWith('
|
|
246
|
+
// Skip empty lines and Alloy root tag
|
|
247
|
+
if (!trimmed || trimmed.startsWith('<Alloy')) {
|
|
198
248
|
continue
|
|
199
249
|
}
|
|
200
250
|
|
|
201
|
-
// Check for
|
|
202
|
-
//
|
|
203
|
-
|
|
251
|
+
// Check for "--" inside XML comments (illegal in XML spec)
|
|
252
|
+
// e.g. <!-- Section: --modules Option --> is invalid because of the "--" before "modules"
|
|
253
|
+
if (trimmed.includes('<!--')) {
|
|
254
|
+
const commentBody = trimmed.replace(/<!--/, '').replace(/-->.*$/, '')
|
|
255
|
+
if (/--/.test(commentBody)) {
|
|
256
|
+
const dashMatch = commentBody.match(/--(\S*)/)
|
|
257
|
+
const offender = dashMatch ? `--${dashMatch[1]}` : '--'
|
|
258
|
+
throwPreValidationError({
|
|
259
|
+
relativePath,
|
|
260
|
+
lineNumber: i + 1,
|
|
261
|
+
lineContent: trimmed,
|
|
262
|
+
message: `XML comment contains illegal "--" sequence ("${offender}")`,
|
|
263
|
+
fix: `Replace "--" with "—" (em-dash) or reword the comment to avoid double dashes`
|
|
264
|
+
})
|
|
265
|
+
}
|
|
266
|
+
continue
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Check for opening tag without tag name: "< class=..." or "< id=..."
|
|
270
|
+
if (/^<\s+(class|id|onClick|onOpen|onClose|height|width|backgroundColor|color|font|text|hintText|imageUrl)=/.test(trimmed)) {
|
|
271
|
+
throwPreValidationError({
|
|
272
|
+
relativePath,
|
|
273
|
+
lineNumber: i + 1,
|
|
274
|
+
lineContent: trimmed,
|
|
275
|
+
message: 'Opening tag is missing its tag name',
|
|
276
|
+
fix: `Add the tag name after "<", e.g. "<View ${trimmed.slice(1).trim()}"`
|
|
277
|
+
})
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Check for double slash in closing tags: "<//View>"
|
|
281
|
+
const doubleSlash = trimmed.match(/^<\/\/([a-zA-Z0-9_]+)>/)
|
|
282
|
+
if (doubleSlash) {
|
|
283
|
+
throwPreValidationError({
|
|
284
|
+
relativePath,
|
|
285
|
+
lineNumber: i + 1,
|
|
286
|
+
lineContent: trimmed,
|
|
287
|
+
message: `Closing tag "</${doubleSlash[1]}>" has an extra "/"`,
|
|
288
|
+
fix: `Change "${trimmed}" to "</${doubleSlash[1]}>"`
|
|
289
|
+
})
|
|
290
|
+
}
|
|
204
291
|
|
|
205
|
-
|
|
292
|
+
// Check for tags without opening < (common mistake: Label, View, etc. without <)
|
|
293
|
+
// Pattern: "Label id=", "View class=","Button onClick=", etc. WITHOUT opening <
|
|
294
|
+
if (/^[A-Z][a-zA-Z0-9_]+\s+(id|class|onClick|onOpen|onClose|height|width|backgroundColor|color|font|text|hintText|imageUrl)=/.test(trimmed)) {
|
|
206
295
|
const tagName = trimmed.split(/\s+|[>=]/)[0]
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
error.tagName = tagName
|
|
216
|
-
|
|
217
|
-
// Print error using logger (still throw, but caller can catch and handle)
|
|
218
|
-
logger.error('XML Syntax Error')
|
|
219
|
-
logger.warn(`File: "${relativePath}"`)
|
|
220
|
-
logger.warn(`Line: ${i + 1}`)
|
|
221
|
-
logger.warn(`Content: "${line.trim()}"`)
|
|
222
|
-
logger.warn(`Error: Tag "<${tagName}>" is missing opening "<"`)
|
|
223
|
-
logger.warn(`Fix: Change "${tagName}" to "<${tagName}>"`)
|
|
296
|
+
throwPreValidationError({
|
|
297
|
+
relativePath,
|
|
298
|
+
lineNumber: i + 1,
|
|
299
|
+
lineContent: trimmed,
|
|
300
|
+
message: `Tag "<${tagName}>" is missing opening "<"`,
|
|
301
|
+
fix: `Change "${tagName}" to "<${tagName}>"`
|
|
302
|
+
})
|
|
303
|
+
}
|
|
224
304
|
|
|
225
|
-
|
|
305
|
+
// Check for closing tag with extra slash: "</View/>"
|
|
306
|
+
const closingExtraSlash = trimmed.match(/^<\/([a-zA-Z0-9_]+)\/>/)
|
|
307
|
+
if (closingExtraSlash) {
|
|
308
|
+
throwPreValidationError({
|
|
309
|
+
relativePath,
|
|
310
|
+
lineNumber: i + 1,
|
|
311
|
+
lineContent: trimmed,
|
|
312
|
+
message: `Closing tag "</${closingExtraSlash[1]}>" has an extra "/" at the end`,
|
|
313
|
+
fix: `Change "${trimmed}" to "</${closingExtraSlash[1]}>"`
|
|
314
|
+
})
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Check for space between < and tag name: "< View class=..."
|
|
318
|
+
const spaceBeforeName = trimmed.match(/^<\s+([A-Z][a-zA-Z0-9_]+)\s/)
|
|
319
|
+
if (spaceBeforeName) {
|
|
320
|
+
throwPreValidationError({
|
|
321
|
+
relativePath,
|
|
322
|
+
lineNumber: i + 1,
|
|
323
|
+
lineContent: trimmed,
|
|
324
|
+
message: `Extra space between "<" and tag name "${spaceBeforeName[1]}"`,
|
|
325
|
+
fix: `Change "< ${spaceBeforeName[1]}" to "<${spaceBeforeName[1]}"`
|
|
326
|
+
})
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Check for attribute without = sign: <View class"foo">
|
|
330
|
+
const attrNoEquals = trimmed.match(/^<([a-zA-Z0-9_]+)\s+[a-zA-Z]+"/)
|
|
331
|
+
if (attrNoEquals && !trimmed.includes('=')) {
|
|
332
|
+
throwPreValidationError({
|
|
333
|
+
relativePath,
|
|
334
|
+
lineNumber: i + 1,
|
|
335
|
+
lineContent: trimmed,
|
|
336
|
+
message: `Attribute is missing "=" sign`,
|
|
337
|
+
fix: `Check attributes in this tag — each one needs an "=" before its value`
|
|
338
|
+
})
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Check for backslash in self-closing tag: <Label text="hi" \>
|
|
342
|
+
if (/\\>/.test(trimmed)) {
|
|
343
|
+
throwPreValidationError({
|
|
344
|
+
relativePath,
|
|
345
|
+
lineNumber: i + 1,
|
|
346
|
+
lineContent: trimmed,
|
|
347
|
+
message: `Tag has a backslash "\\>" instead of forward slash "/>"`,
|
|
348
|
+
fix: `Change "\\>" to "/>"`
|
|
349
|
+
})
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Check for double closing bracket: <View class="foo">>
|
|
353
|
+
if (/>>/.test(trimmed) && !trimmed.includes('<!--')) {
|
|
354
|
+
throwPreValidationError({
|
|
355
|
+
relativePath,
|
|
356
|
+
lineNumber: i + 1,
|
|
357
|
+
lineContent: trimmed,
|
|
358
|
+
message: `Tag has a double ">>" closing bracket`,
|
|
359
|
+
fix: `Remove the extra ">"`
|
|
360
|
+
})
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Check for unclosed opening tag (line ends without > and next line starts a new tag)
|
|
364
|
+
if (trimmed.startsWith('<') && !trimmed.startsWith('</') && !trimmed.startsWith('<!--') && !trimmed.endsWith('>') && !trimmed.endsWith('-->')) {
|
|
365
|
+
const nextTrimmed = (lines[i + 1] || '').trim()
|
|
366
|
+
if (nextTrimmed.startsWith('<')) {
|
|
367
|
+
throwPreValidationError({
|
|
368
|
+
relativePath,
|
|
369
|
+
lineNumber: i + 1,
|
|
370
|
+
lineContent: trimmed,
|
|
371
|
+
message: `Tag is missing its closing ">"`,
|
|
372
|
+
fix: `Add ">" at the end of this tag`
|
|
373
|
+
})
|
|
374
|
+
}
|
|
226
375
|
}
|
|
227
376
|
}
|
|
228
377
|
|
|
229
378
|
return false
|
|
230
379
|
}
|
|
231
380
|
|
|
381
|
+
function throwPreValidationError({ relativePath, lineNumber, lineContent, message, fix }) {
|
|
382
|
+
const error = new Error(`XML Syntax Error in ${relativePath}:${lineNumber}`)
|
|
383
|
+
error.isPreValidationError = true
|
|
384
|
+
error.filePath = relativePath
|
|
385
|
+
error.lineNumber = lineNumber
|
|
386
|
+
error.lineContent = lineContent
|
|
387
|
+
|
|
388
|
+
logger.error('XML Syntax Error\n')
|
|
389
|
+
logger.info(`File: "${relativePath}"`)
|
|
390
|
+
logger.info(`Line: ${lineNumber}`)
|
|
391
|
+
logger.info(`Content: "${lineContent}"\n`)
|
|
392
|
+
logger.error(message)
|
|
393
|
+
logger.info(chalk.green(`Fix: ${fix}\n`))
|
|
394
|
+
|
|
395
|
+
throw error
|
|
396
|
+
}
|
|
397
|
+
|
|
232
398
|
/**
|
|
233
399
|
* Extract classes from file content
|
|
234
400
|
* COPIED exactly from original extractClasses() function
|
|
@@ -383,7 +383,7 @@ export function combineAllValues(base, defaultTheme) {
|
|
|
383
383
|
allValues.contentWidth = combineKeys(configFile.theme, base.width, 'contentWidth')
|
|
384
384
|
allValues.countDownDuration = combineKeys(configFile.theme, base.spacing, 'countDownDuration')
|
|
385
385
|
allValues.elevation = combineKeys(configFile.theme, base.spacing, 'elevation')
|
|
386
|
-
allValues.fontFamily = combineKeys(configFile.theme,
|
|
386
|
+
allValues.fontFamily = combineKeys(configFile.theme, {}, 'fontFamily')
|
|
387
387
|
allValues.fontSize = combineKeys(configFile.theme, base.fontSize, 'fontSize')
|
|
388
388
|
allValues.fontWeight = combineKeys(configFile.theme, defaultTheme.fontWeight, 'fontWeight')
|
|
389
389
|
allValues.gap = combineKeys(configFile.theme, base.spacing, 'gap')
|
|
@@ -26,18 +26,53 @@ function removeFractions(modifiersAndValues, extras = []) {
|
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* Font family property for text components
|
|
29
|
+
*
|
|
30
|
+
* Built-in platform defaults:
|
|
31
|
+
* - font-sans → Android: 'sans-serif', iOS: 'Helvetica Neue'
|
|
32
|
+
* - font-serif → Android: 'serif', iOS: 'Georgia'
|
|
33
|
+
* - font-mono → 'monospace' (both platforms)
|
|
34
|
+
*
|
|
35
|
+
* User values from config.cjs override defaults cross-platform.
|
|
36
|
+
*
|
|
29
37
|
* @param {Object} modifiersAndValues - Modifier and value pairs
|
|
30
38
|
* @returns {string} Generated styles
|
|
31
39
|
*/
|
|
32
40
|
export function fontFamily(modifiersAndValues) {
|
|
41
|
+
const platformDefaults = {
|
|
42
|
+
sans: { ios: 'Helvetica Neue', android: 'sans-serif' },
|
|
43
|
+
serif: { ios: 'Georgia', android: 'serif' }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const crossPlatformDefaults = { mono: 'monospace' }
|
|
47
|
+
|
|
48
|
+
const defaults = { ...modifiersAndValues }
|
|
49
|
+
const ios = {}
|
|
50
|
+
const android = {}
|
|
51
|
+
|
|
52
|
+
_.each(crossPlatformDefaults, (value, key) => {
|
|
53
|
+
if (!(key in defaults)) {
|
|
54
|
+
defaults[key] = value
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
_.each(platformDefaults, (platforms, key) => {
|
|
59
|
+
if (!(key in defaults)) {
|
|
60
|
+
ios[key] = platforms.ios
|
|
61
|
+
android[key] = platforms.android
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
const selectorsAndValues = {}
|
|
66
|
+
if (!_.isEmpty(defaults)) selectorsAndValues.default = defaults
|
|
67
|
+
if (!_.isEmpty(ios)) selectorsAndValues.ios = ios
|
|
68
|
+
if (!_.isEmpty(android)) selectorsAndValues.android = android
|
|
69
|
+
|
|
33
70
|
return processProperties({
|
|
34
71
|
prop: 'fontFamily',
|
|
35
72
|
modules: 'Ti.UI.ActivityIndicator, Ti.UI.Button, Ti.UI.Label, Ti.UI.ListItem, Ti.UI.Picker, Ti.UI.PickerColumn, Ti.UI.PickerRow, Ti.UI.ProgressBar, Ti.UI.Switch, Ti.UI.TableViewRow, Ti.UI.TextArea, Ti.UI.TextField'
|
|
36
73
|
}, {
|
|
37
74
|
font: '{ font: { fontFamily: {value} } }'
|
|
38
|
-
},
|
|
39
|
-
default: modifiersAndValues
|
|
40
|
-
})
|
|
75
|
+
}, selectorsAndValues)
|
|
41
76
|
}
|
|
42
77
|
|
|
43
78
|
/**
|