purgetss 7.7.1 → 7.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/bin/purgetss +10 -0
- package/dist/purgetss.ui.js +1 -1
- 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/{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
|
@@ -1,39 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* PurgeTSS v7.1.0 - Core Builder: Tailwind Helpers
|
|
3
|
-
* Helper functions for building Tailwind CSS
|
|
4
|
-
*
|
|
5
|
-
* COPIED from src/index.js during refactorization - NO CHANGES to logic.
|
|
6
3
|
*
|
|
7
4
|
* @since 7.1.0
|
|
8
5
|
* @author César Estrada
|
|
9
6
|
*/
|
|
10
7
|
|
|
11
|
-
import _ from 'lodash'
|
|
12
|
-
import defaultColors from 'tailwindcss/colors.js'
|
|
13
|
-
|
|
14
|
-
// Import functions from their new modular locations
|
|
15
|
-
import { getConfigFile } from '../../shared/config-manager.js'
|
|
16
|
-
import { removeDeprecatedColors, fixPercentages } from '../../shared/helpers.js'
|
|
17
|
-
|
|
18
|
-
// Get config once for this module
|
|
19
|
-
const configFile = getConfigFile()
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Remove fit, max, min values from width, height and spacing objects
|
|
23
|
-
* @param {Object} theObject - Object with width, height, spacing properties
|
|
24
|
-
*/
|
|
25
|
-
export function removeFitMaxMin(theObject) {
|
|
26
|
-
delete theObject.width.fit
|
|
27
|
-
delete theObject.width.max
|
|
28
|
-
delete theObject.width.min
|
|
29
|
-
delete theObject.height.fit
|
|
30
|
-
delete theObject.height.max
|
|
31
|
-
delete theObject.height.min
|
|
32
|
-
delete theObject.spacing.fit
|
|
33
|
-
delete theObject.spacing.max
|
|
34
|
-
delete theObject.spacing.min
|
|
35
|
-
}
|
|
36
|
-
|
|
37
8
|
/**
|
|
38
9
|
* Combine keys from theme and extend, with fallback to base values
|
|
39
10
|
* @param {Object} values - Theme values object
|
|
@@ -44,418 +15,3 @@ export function removeFitMaxMin(theObject) {
|
|
|
44
15
|
export function combineKeys(values, base, key) {
|
|
45
16
|
return (values[key]) ? { ...values[key], ...values.extend[key] } : { ...base, ...values.extend[key] }
|
|
46
17
|
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Get base values for Tailwind building
|
|
50
|
-
* This function prepares all the base theme values needed for building
|
|
51
|
-
* @param {Object} defaultTheme - Default Tailwind theme
|
|
52
|
-
* @returns {Object} Base values object
|
|
53
|
-
*/
|
|
54
|
-
export function getBaseValues(defaultTheme) {
|
|
55
|
-
const defaultThemeWidth = defaultTheme.width({ theme: () => (defaultTheme.spacing) })
|
|
56
|
-
const defaultThemeHeight = defaultTheme.height({ theme: () => (defaultTheme.spacing) })
|
|
57
|
-
|
|
58
|
-
removeDeprecatedColors(defaultColors)
|
|
59
|
-
|
|
60
|
-
// !Prepare values
|
|
61
|
-
const tiResets = { full: '100%' }
|
|
62
|
-
const allWidthsCombined = (configFile.theme.spacing) ? { ...configFile.theme.spacing, ...tiResets } : { ...defaultThemeWidth }
|
|
63
|
-
const allHeightsCombined = (configFile.theme.spacing) ? { ...configFile.theme.spacing, ...tiResets } : { ...defaultThemeHeight }
|
|
64
|
-
const allSpacingCombined = (configFile.theme.spacing) ? { ...configFile.theme.spacing, ...tiResets } : { ...defaultThemeWidth, ...defaultThemeHeight }
|
|
65
|
-
|
|
66
|
-
const themeOrDefaultValues = {
|
|
67
|
-
width: configFile.theme.width ?? allWidthsCombined,
|
|
68
|
-
height: configFile.theme.height ?? allHeightsCombined,
|
|
69
|
-
spacing: configFile.theme.spacing ?? allSpacingCombined,
|
|
70
|
-
fontSize: configFile.theme.spacing ?? defaultTheme.fontSize,
|
|
71
|
-
colors: configFile.theme.colors ?? { transparent: 'transparent', ...defaultColors }
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// ! Remove unnecessary values
|
|
75
|
-
removeFitMaxMin(themeOrDefaultValues)
|
|
76
|
-
|
|
77
|
-
// ! Merge with extend values
|
|
78
|
-
const base = {
|
|
79
|
-
width: { ...themeOrDefaultValues.spacing, ...configFile.theme.extend.spacing, ...themeOrDefaultValues.width, ...configFile.theme.extend.width },
|
|
80
|
-
height: { ...themeOrDefaultValues.spacing, ...configFile.theme.extend.spacing, ...themeOrDefaultValues.height, ...configFile.theme.extend.height },
|
|
81
|
-
colors: { ...themeOrDefaultValues.colors, ...configFile.theme.extend.colors },
|
|
82
|
-
spacing: { ...themeOrDefaultValues.spacing, ...configFile.theme.extend.spacing },
|
|
83
|
-
fontSize: { ...themeOrDefaultValues.fontSize, ...configFile.theme.extend.spacing, ...configFile.theme.extend.fontSize },
|
|
84
|
-
columns: { 0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, 8: 8, 9: 9, 10: 10, 11: 11, 12: 12 },
|
|
85
|
-
delay: { 0: '0ms', 25: '25ms', 50: '50ms', 250: '250ms', 350: '350ms', 400: '400ms', 450: '450ms', 600: '600ms', 800: '800ms', 900: '900ms', 2000: '2000ms', 3000: '3000ms', 4000: '4000ms', 5000: '5000ms' }
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
fixPercentages(base.width)
|
|
89
|
-
fixPercentages(base.height)
|
|
90
|
-
fixPercentages(base.spacing)
|
|
91
|
-
|
|
92
|
-
return base
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Combine all values for Tailwind building - MASSIVE function with all Titanium properties
|
|
97
|
-
* This function builds the complete values object used in legacy Tailwind building
|
|
98
|
-
* @param {Object} base - Base values from getBaseValues()
|
|
99
|
-
* @param {Object} defaultTheme - Default Tailwind theme
|
|
100
|
-
* @returns {Object} Complete values object for building
|
|
101
|
-
*/
|
|
102
|
-
export function combineAllValues(base, defaultTheme) {
|
|
103
|
-
const allValues = {}
|
|
104
|
-
|
|
105
|
-
// ! Custom Window, View and ImageView
|
|
106
|
-
// Merge extend values into theme (same as colors, spacing, etc.)
|
|
107
|
-
_.each(['Window', 'View', 'ImageView'], comp => {
|
|
108
|
-
if (configFile.theme.extend[comp]) {
|
|
109
|
-
configFile.theme[comp] = _.merge({}, configFile.theme[comp], configFile.theme.extend[comp])
|
|
110
|
-
delete configFile.theme.extend[comp]
|
|
111
|
-
}
|
|
112
|
-
// Normalize shorthand: { apply: '...' } → { default: { apply: '...' } }
|
|
113
|
-
if (configFile.theme[comp] && configFile.theme[comp].apply && !configFile.theme[comp].default) {
|
|
114
|
-
configFile.theme[comp] = { default: configFile.theme[comp] }
|
|
115
|
-
}
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
// Merge user config WITH defaults, then write back to configFile.theme
|
|
119
|
-
// so that downstream functions pick up the full merged object
|
|
120
|
-
configFile.theme.Window = _.merge({ default: { backgroundColor: '#FFFFFF' } }, configFile.theme.Window)
|
|
121
|
-
configFile.theme.ImageView = _.merge({ ios: { hires: true } }, configFile.theme.ImageView)
|
|
122
|
-
configFile.theme.View = _.merge({ default: { width: 'Ti.UI.SIZE', height: 'Ti.UI.SIZE' } }, configFile.theme.View)
|
|
123
|
-
|
|
124
|
-
allValues.Window = configFile.theme.Window
|
|
125
|
-
allValues.ImageView = configFile.theme.ImageView
|
|
126
|
-
allValues.View = configFile.theme.View
|
|
127
|
-
|
|
128
|
-
// ! Width, height and margin properties
|
|
129
|
-
// INFO: sizingProperties: For glossary generator only... Do not move from this position.
|
|
130
|
-
allValues.sizingProperties = {}
|
|
131
|
-
|
|
132
|
-
allValues.height = base.height
|
|
133
|
-
allValues.width = base.width
|
|
134
|
-
allValues.margin = combineKeys(configFile.theme, base.spacing, 'margin')
|
|
135
|
-
allValues.marginAlternate = combineKeys(configFile.theme, base.spacing, 'margin')
|
|
136
|
-
|
|
137
|
-
// ! Properties with constant values
|
|
138
|
-
// INFO: constantProperties: For glossary generator only... Do not move from this position.
|
|
139
|
-
allValues.constantProperties = {}
|
|
140
|
-
|
|
141
|
-
// allValues.audioStreamType = {};
|
|
142
|
-
// allValues.category = {};
|
|
143
|
-
allValues.accessibilityHidden = {}
|
|
144
|
-
allValues.accessoryType = {}
|
|
145
|
-
allValues.activeIconIsMask = {}
|
|
146
|
-
allValues.activityEnterTransition = {}
|
|
147
|
-
allValues.activityExitTransition = {}
|
|
148
|
-
allValues.activityIndicatorStyle = {}
|
|
149
|
-
allValues.activityReenterTransition = {}
|
|
150
|
-
allValues.activityReturnTransition = {}
|
|
151
|
-
allValues.activitySharedElementEnterTransition = {}
|
|
152
|
-
allValues.activitySharedElementExitTransition = {}
|
|
153
|
-
allValues.activitySharedElementReenterTransition = {}
|
|
154
|
-
allValues.activitySharedElementReturnTransition = {}
|
|
155
|
-
allValues.alertDialogStyle = {}
|
|
156
|
-
allValues.allowsBackForwardNavigationGestures = {}
|
|
157
|
-
allValues.allowsLinkPreview = {}
|
|
158
|
-
allValues.allowsMultipleSelectionDuringEditing = {}
|
|
159
|
-
allValues.allowsMultipleSelectionInteraction = {}
|
|
160
|
-
allValues.allowsSelection = {}
|
|
161
|
-
allValues.allowsSelectionDuringEditing = {}
|
|
162
|
-
allValues.allowUserCustomization = {}
|
|
163
|
-
allValues.anchorPoint = {}
|
|
164
|
-
allValues.autoAdjustScrollViewInsets = {}
|
|
165
|
-
allValues.autocapitalization = {}
|
|
166
|
-
allValues.autocorrect = {}
|
|
167
|
-
allValues.autofillType = {}
|
|
168
|
-
allValues.autoLink = {}
|
|
169
|
-
allValues.autoreverse = {}
|
|
170
|
-
allValues.autorotate = {}
|
|
171
|
-
allValues.backgroundBlendMode = {}
|
|
172
|
-
allValues.backgroundLinearGradient = {}
|
|
173
|
-
allValues.backgroundRadialGradient = {}
|
|
174
|
-
allValues.backgroundRepeat = {}
|
|
175
|
-
allValues.borderStyle = {}
|
|
176
|
-
allValues.bubbleParent = {}
|
|
177
|
-
allValues.buttonStyle = {}
|
|
178
|
-
allValues.cacheMode = {}
|
|
179
|
-
allValues.cachePolicy = {}
|
|
180
|
-
allValues.calendarViewShown = {}
|
|
181
|
-
allValues.canCancelEvents = {}
|
|
182
|
-
allValues.cancelable = {}
|
|
183
|
-
allValues.canceledOnTouchOutside = {}
|
|
184
|
-
allValues.canDelete = {}
|
|
185
|
-
allValues.canEdit = {}
|
|
186
|
-
allValues.canInsert = {}
|
|
187
|
-
allValues.canMove = {}
|
|
188
|
-
allValues.canScroll = {}
|
|
189
|
-
allValues.caseInsensitiveSearch = {}
|
|
190
|
-
allValues.checkable = {}
|
|
191
|
-
allValues.clearButtonMode = {}
|
|
192
|
-
allValues.clearOnEdit = {}
|
|
193
|
-
allValues.clipMode = {}
|
|
194
|
-
allValues.constraint = {}
|
|
195
|
-
allValues.contentHeightAndWidth = {}
|
|
196
|
-
allValues.curve = {}
|
|
197
|
-
allValues.datePickerStyle = {}
|
|
198
|
-
allValues.defaultItemTemplate = {}
|
|
199
|
-
allValues.dimBackgroundForSearch = {}
|
|
200
|
-
allValues.disableBounce = {}
|
|
201
|
-
allValues.disableContextMenu = {}
|
|
202
|
-
allValues.displayCaps = {}
|
|
203
|
-
allValues.displayHomeAsUp = {}
|
|
204
|
-
allValues.draggingType = {}
|
|
205
|
-
allValues.drawerIndicatorEnabled = {}
|
|
206
|
-
allValues.drawerLockMode = {}
|
|
207
|
-
allValues.dropShadow = {}
|
|
208
|
-
allValues.duration = {}
|
|
209
|
-
allValues.editable = {}
|
|
210
|
-
allValues.editing = {}
|
|
211
|
-
allValues.ellipsize = {}
|
|
212
|
-
allValues.enableCopy = {}
|
|
213
|
-
allValues.enabled = {}
|
|
214
|
-
allValues.enableJavascriptInterface = {}
|
|
215
|
-
allValues.enableReturnKey = {}
|
|
216
|
-
allValues.enableZoomControls = {}
|
|
217
|
-
allValues.exitOnClose = {}
|
|
218
|
-
allValues.extendBackground = {}
|
|
219
|
-
allValues.extendEdges = {}
|
|
220
|
-
allValues.extendSafeArea = {}
|
|
221
|
-
allValues.fastScroll = {}
|
|
222
|
-
allValues.filterAnchored = {}
|
|
223
|
-
allValues.filterAttribute = {}
|
|
224
|
-
allValues.filterCaseInsensitive = {}
|
|
225
|
-
allValues.filterTouchesWhenObscured = {}
|
|
226
|
-
allValues.flags = {}
|
|
227
|
-
allValues.flagSecure = {}
|
|
228
|
-
allValues.flip = {}
|
|
229
|
-
allValues.focusable = {}
|
|
230
|
-
allValues.fontStyle = {}
|
|
231
|
-
allValues.footerDividersEnabled = {}
|
|
232
|
-
allValues.format24 = {}
|
|
233
|
-
allValues.fullscreen = {}
|
|
234
|
-
allValues.gravity = {}
|
|
235
|
-
allValues.gridColumnsRowsStartEnd = {}
|
|
236
|
-
allValues.gridFlow = {}
|
|
237
|
-
allValues.gridSystem = {}
|
|
238
|
-
allValues.hasCheck = {}
|
|
239
|
-
allValues.hasChild = {}
|
|
240
|
-
allValues.hasDetail = {}
|
|
241
|
-
allValues.headerDividersEnabled = {}
|
|
242
|
-
allValues.hiddenBehavior = {}
|
|
243
|
-
allValues.hideLoadIndicator = {}
|
|
244
|
-
allValues.hidesBackButton = {}
|
|
245
|
-
allValues.hidesBarsOnSwipe = {}
|
|
246
|
-
allValues.hidesBarsOnTap = {}
|
|
247
|
-
allValues.hidesBarsWhenKeyboardAppears = {}
|
|
248
|
-
allValues.hideSearchOnSelection = {}
|
|
249
|
-
allValues.hideShadow = {}
|
|
250
|
-
allValues.hidesSearchBarWhenScrolling = {}
|
|
251
|
-
allValues.hintType = {}
|
|
252
|
-
allValues.hires = {}
|
|
253
|
-
allValues.homeButtonEnabled = {}
|
|
254
|
-
allValues.homeIndicatorAutoHidden = {}
|
|
255
|
-
allValues.horizontalWrap = {}
|
|
256
|
-
allValues.html = {}
|
|
257
|
-
allValues.icon = {}
|
|
258
|
-
allValues.iconified = {}
|
|
259
|
-
allValues.iconifiedByDefault = {}
|
|
260
|
-
allValues.iconIsMask = {}
|
|
261
|
-
allValues.ignoreSslError = {}
|
|
262
|
-
allValues.imageTouchFeedback = {}
|
|
263
|
-
allValues.includeFontPadding = {}
|
|
264
|
-
allValues.includeOpaqueBars = {}
|
|
265
|
-
allValues.keepScreenOn = {}
|
|
266
|
-
allValues.keepSectionsInSearch = {}
|
|
267
|
-
allValues.keyboardAppearance = {}
|
|
268
|
-
allValues.keyboardDismissMode = {}
|
|
269
|
-
allValues.keyboardDisplayRequiresUserAction = {}
|
|
270
|
-
allValues.keyboardType = {}
|
|
271
|
-
allValues.largeTitleDisplayMode = {}
|
|
272
|
-
allValues.largeTitleEnabled = {}
|
|
273
|
-
allValues.layout = {}
|
|
274
|
-
allValues.lazyLoadingEnabled = {}
|
|
275
|
-
allValues.leftButtonMode = {}
|
|
276
|
-
allValues.leftDrawerLockMode = {}
|
|
277
|
-
allValues.lightTouchEnabled = {}
|
|
278
|
-
allValues.listViewStyle = {}
|
|
279
|
-
allValues.loginKeyboardType = {}
|
|
280
|
-
allValues.loginReturnKeyType = {}
|
|
281
|
-
allValues.mixedContentMode = {}
|
|
282
|
-
allValues.modal = {}
|
|
283
|
-
allValues.moveable = {}
|
|
284
|
-
allValues.moving = {}
|
|
285
|
-
allValues.nativeSpinner = {}
|
|
286
|
-
allValues.navBarHidden = {}
|
|
287
|
-
allValues.navigationMode = {}
|
|
288
|
-
allValues.orientationModes = {}
|
|
289
|
-
allValues.overlayEnabled = {}
|
|
290
|
-
allValues.overrideCurrentAnimation = {}
|
|
291
|
-
allValues.overScrollMode = {}
|
|
292
|
-
allValues.showPagingControl = {}
|
|
293
|
-
allValues.pagingControlOnTop = {}
|
|
294
|
-
allValues.passwordKeyboardType = {}
|
|
295
|
-
allValues.passwordMask = {}
|
|
296
|
-
allValues.pickerType = {}
|
|
297
|
-
allValues.placement = {}
|
|
298
|
-
allValues.pluginState = {}
|
|
299
|
-
allValues.preventCornerOverlap = {}
|
|
300
|
-
allValues.preventDefaultImage = {}
|
|
301
|
-
allValues.previewActionStyle = {}
|
|
302
|
-
allValues.progressBarStyle = {}
|
|
303
|
-
allValues.progressIndicatorType = {}
|
|
304
|
-
allValues.pruneSectionsOnEdit = {}
|
|
305
|
-
allValues.requestedOrientation = {}
|
|
306
|
-
allValues.resultsSeparatorStyle = {}
|
|
307
|
-
allValues.returnKeyType = {}
|
|
308
|
-
allValues.reverse = {}
|
|
309
|
-
allValues.rightButtonMode = {}
|
|
310
|
-
allValues.rightDrawerLockMode = {}
|
|
311
|
-
allValues.scalesPageToFit = {}
|
|
312
|
-
allValues.scrollable = {}
|
|
313
|
-
allValues.scrollIndicators = {}
|
|
314
|
-
allValues.scrollIndicatorStyle = {}
|
|
315
|
-
allValues.scrollingEnabled = {}
|
|
316
|
-
allValues.scrollsToTop = {}
|
|
317
|
-
allValues.scrollType = {}
|
|
318
|
-
allValues.searchAsChild = {}
|
|
319
|
-
allValues.searchBarStyle = {}
|
|
320
|
-
allValues.searchHidden = {}
|
|
321
|
-
allValues.selectionGranularity = {}
|
|
322
|
-
allValues.selectionOpens = {}
|
|
323
|
-
allValues.selectionStyle = {}
|
|
324
|
-
allValues.separatorStyle = {}
|
|
325
|
-
allValues.shiftMode = {}
|
|
326
|
-
allValues.showAsAction = {}
|
|
327
|
-
allValues.showBookmark = {}
|
|
328
|
-
allValues.showCancel = {}
|
|
329
|
-
allValues.showHorizontalScrollIndicator = {}
|
|
330
|
-
allValues.showSearchBarInNavBar = {}
|
|
331
|
-
allValues.showSelectionCheck = {}
|
|
332
|
-
allValues.showUndoRedoActions = {}
|
|
333
|
-
allValues.showVerticalScrollIndicator = {}
|
|
334
|
-
allValues.smoothScrollOnTabClick = {}
|
|
335
|
-
allValues.statusBarStyle = {}
|
|
336
|
-
allValues.submitEnabled = {}
|
|
337
|
-
allValues.suppressReturn = {}
|
|
338
|
-
allValues.sustainedPerformanceMode = {}
|
|
339
|
-
allValues.swipeToClose = {}
|
|
340
|
-
allValues.switchStyle = {}
|
|
341
|
-
allValues.systemButton = {}
|
|
342
|
-
allValues.tabBarHidden = {}
|
|
343
|
-
allValues.tabbedBarStyle = {}
|
|
344
|
-
allValues.tabGroupStyle = {}
|
|
345
|
-
allValues.tableViewStyle = {}
|
|
346
|
-
allValues.tabsTranslucent = {}
|
|
347
|
-
allValues.textAlign = {}
|
|
348
|
-
allValues.theme = {}
|
|
349
|
-
allValues.titleAttributesShadow = {}
|
|
350
|
-
allValues.toolbarEnabled = {}
|
|
351
|
-
allValues.touchEnabled = {}
|
|
352
|
-
allValues.touchFeedback = {}
|
|
353
|
-
allValues.translucent = {}
|
|
354
|
-
allValues.useCompatPadding = {}
|
|
355
|
-
allValues.useSpinner = {}
|
|
356
|
-
allValues.verticalAlign = {}
|
|
357
|
-
allValues.verticalBounce = {}
|
|
358
|
-
allValues.viewShadow = {}
|
|
359
|
-
allValues.visible = {}
|
|
360
|
-
allValues.willHandleTouches = {}
|
|
361
|
-
allValues.willScrollOnStatusTap = {}
|
|
362
|
-
allValues.windowPixelFormat = {}
|
|
363
|
-
allValues.windowSoftInputMode = {}
|
|
364
|
-
allValues.wobble = {}
|
|
365
|
-
|
|
366
|
-
// ! Configurable properties
|
|
367
|
-
// INFO: configurableProperties: For glossary generator only... Do not move from this position.
|
|
368
|
-
allValues.configurableProperties = {}
|
|
369
|
-
|
|
370
|
-
allValues.activeTab = combineKeys(configFile.theme, base.spacing, 'activeTab')
|
|
371
|
-
allValues.backgroundLeftCap = combineKeys(configFile.theme, base.spacing, 'backgroundLeftCap')
|
|
372
|
-
allValues.backgroundPaddingBottom = combineKeys(configFile.theme, base.spacing, 'backgroundPaddingBottom')
|
|
373
|
-
allValues.backgroundPaddingLeft = combineKeys(configFile.theme, base.spacing, 'backgroundPaddingLeft')
|
|
374
|
-
allValues.backgroundPaddingRight = combineKeys(configFile.theme, base.spacing, 'backgroundPaddingRight')
|
|
375
|
-
allValues.backgroundPaddingTop = combineKeys(configFile.theme, base.spacing, 'backgroundPaddingTop')
|
|
376
|
-
allValues.backgroundTopCap = combineKeys(configFile.theme, base.spacing, 'backgroundTopCap')
|
|
377
|
-
allValues.borderRadius = combineKeys(configFile.theme, base.spacing, 'borderRadius')
|
|
378
|
-
allValues.borderWidth = combineKeys(configFile.theme, base.spacing, 'borderWidth')
|
|
379
|
-
allValues.bottomNavigation = combineKeys(configFile.theme, base.spacing, 'bottomNavigation')
|
|
380
|
-
allValues.cacheSize = combineKeys(configFile.theme, base.spacing, 'cacheSize')
|
|
381
|
-
allValues.columnCount = combineKeys(configFile.theme, { 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, 8: 8, 9: 9, 10: 10, 11: 11, 12: 12 }, 'columnCount')
|
|
382
|
-
allValues.contentHeight = combineKeys(configFile.theme, base.height, 'contentHeight')
|
|
383
|
-
allValues.contentWidth = combineKeys(configFile.theme, base.width, 'contentWidth')
|
|
384
|
-
allValues.countDownDuration = combineKeys(configFile.theme, base.spacing, 'countDownDuration')
|
|
385
|
-
allValues.elevation = combineKeys(configFile.theme, base.spacing, 'elevation')
|
|
386
|
-
allValues.fontFamily = combineKeys(configFile.theme, {}, 'fontFamily')
|
|
387
|
-
allValues.fontSize = combineKeys(configFile.theme, base.fontSize, 'fontSize')
|
|
388
|
-
allValues.fontWeight = combineKeys(configFile.theme, defaultTheme.fontWeight, 'fontWeight')
|
|
389
|
-
allValues.gap = combineKeys(configFile.theme, base.spacing, 'gap')
|
|
390
|
-
allValues.indentionLevel = combineKeys(configFile.theme, base.spacing, 'indentionLevel')
|
|
391
|
-
allValues.keyboardToolbarHeight = combineKeys(configFile.theme, base.spacing, 'keyboardToolbarHeight')
|
|
392
|
-
allValues.leftButtonPadding = combineKeys(configFile.theme, base.spacing, 'leftButtonPadding')
|
|
393
|
-
allValues.leftWidth = combineKeys(configFile.theme, base.width, 'leftWidth')
|
|
394
|
-
allValues.lines = combineKeys(configFile.theme, { 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, 8: 8, 9: 9, 10: 10 }, 'lines')
|
|
395
|
-
allValues.maxElevation = combineKeys(configFile.theme, base.spacing, 'maxElevation')
|
|
396
|
-
allValues.maxLines = combineKeys(configFile.theme, { 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, 8: 8, 9: 9, 10: 10 }, 'maxLines')
|
|
397
|
-
allValues.maxRowHeight = combineKeys(configFile.theme, base.height, 'maxRowHeight')
|
|
398
|
-
allValues.maxZoomScale = combineKeys(configFile.theme, { 0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5 }, 'maxZoomScale')
|
|
399
|
-
allValues.minimumFontSize = combineKeys(configFile.theme, base.fontSize, 'minimumFontSize')
|
|
400
|
-
allValues.minRowHeight = combineKeys(configFile.theme, base.height, 'minRowHeight')
|
|
401
|
-
allValues.minZoomScale = combineKeys(configFile.theme, { 0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5 }, 'minZoomScale')
|
|
402
|
-
allValues.offsets = combineKeys(configFile.theme, base.spacing, 'offsets')
|
|
403
|
-
allValues.opacity = combineKeys(configFile.theme, defaultTheme.opacity, 'opacity')
|
|
404
|
-
allValues.padding = combineKeys(configFile.theme, base.spacing, 'padding')
|
|
405
|
-
allValues.pagingControlAlpha = combineKeys(configFile.theme, defaultTheme.opacity, 'pagingControlAlpha')
|
|
406
|
-
allValues.pagingControlHeight = combineKeys(configFile.theme, base.height, 'pagingControlHeight')
|
|
407
|
-
allValues.pagingControlTimeout = combineKeys(configFile.theme, base.delay, 'pagingControlTimeout')
|
|
408
|
-
allValues.repeat = combineKeys(configFile.theme, { 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, 8: 8, 9: 9, 10: 10, infinite: -1 }, 'repeat')
|
|
409
|
-
allValues.repeatCount = combineKeys(configFile.theme, { 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, 8: 8, 9: 9, 10: 10, infinite: -1 }, 'repeatCount')
|
|
410
|
-
allValues.rightButtonPadding = combineKeys(configFile.theme, base.spacing, 'rightButtonPadding')
|
|
411
|
-
allValues.rightWidth = combineKeys(configFile.theme, base.width, 'rightWidth')
|
|
412
|
-
allValues.rotate = combineKeys(configFile.theme, defaultTheme.rotate, 'rotate')
|
|
413
|
-
|
|
414
|
-
// ! Custom Color properties
|
|
415
|
-
// INFO: colorProperties: For glossary generator only... Do not move from this position.
|
|
416
|
-
allValues.colorProperties = {}
|
|
417
|
-
|
|
418
|
-
allValues.activeTintColor = combineKeys(configFile.theme, base.colors, 'activeTintColor')
|
|
419
|
-
allValues.activeTitleColor = combineKeys(configFile.theme, base.colors, 'activeTitleColor')
|
|
420
|
-
allValues.backgroundColor = combineKeys(configFile.theme, base.colors, 'backgroundColor')
|
|
421
|
-
allValues.backgroundDisabledColor = combineKeys(configFile.theme, base.colors, 'backgroundDisabledColor')
|
|
422
|
-
allValues.backgroundFocusedColor = combineKeys(configFile.theme, base.colors, 'backgroundFocusedColor')
|
|
423
|
-
allValues.backgroundGradient = combineKeys(configFile.theme, base.colors, 'backgroundGradient')
|
|
424
|
-
allValues.backgroundSelectedColor = combineKeys(configFile.theme, base.colors, 'backgroundSelectedColor')
|
|
425
|
-
allValues.backgroundSelectedGradient = combineKeys(configFile.theme, base.colors, 'backgroundSelectedGradient')
|
|
426
|
-
allValues.badgeColor = combineKeys(configFile.theme, base.colors, 'badgeColor')
|
|
427
|
-
allValues.barColor = combineKeys(configFile.theme, base.colors, 'barColor')
|
|
428
|
-
allValues.borderColor = combineKeys(configFile.theme, base.colors, 'borderColor')
|
|
429
|
-
allValues.currentPageIndicatorColor = combineKeys(configFile.theme, base.colors, 'currentPageIndicatorColor')
|
|
430
|
-
allValues.dateTimeColor = combineKeys(configFile.theme, base.colors, 'dateTimeColor')
|
|
431
|
-
allValues.disabledColor = combineKeys(configFile.theme, base.colors, 'disabledColor')
|
|
432
|
-
allValues.highlightedColor = combineKeys(configFile.theme, base.colors, 'highlightedColor')
|
|
433
|
-
allValues.hintTextColor = combineKeys(configFile.theme, base.colors, 'hintTextColor')
|
|
434
|
-
allValues.imageTouchFeedbackColor = combineKeys(configFile.theme, base.colors, 'imageTouchFeedbackColor')
|
|
435
|
-
allValues.pageIndicatorColor = combineKeys(configFile.theme, base.colors, 'pageIndicatorColor')
|
|
436
|
-
allValues.pagingControlColor = combineKeys(configFile.theme, base.colors, 'pagingControlColor')
|
|
437
|
-
allValues.pullBackgroundColor = combineKeys(configFile.theme, base.colors, 'pullBackgroundColor')
|
|
438
|
-
allValues.resultsBackgroundColor = combineKeys(configFile.theme, base.colors, 'resultsBackgroundColor')
|
|
439
|
-
allValues.resultsSeparatorColor = combineKeys(configFile.theme, base.colors, 'resultsSeparatorColor')
|
|
440
|
-
allValues.selectedBackgroundColor = combineKeys(configFile.theme, base.colors, 'selectedBackgroundColor')
|
|
441
|
-
allValues.selectedBackgroundGradient = combineKeys(configFile.theme, base.colors, 'selectedBackgroundGradient')
|
|
442
|
-
allValues.selectedColor = combineKeys(configFile.theme, base.colors, 'selectedColor')
|
|
443
|
-
allValues.separatorColor = combineKeys(configFile.theme, base.colors, 'separatorColor')
|
|
444
|
-
allValues.shadowColor = combineKeys(configFile.theme, base.colors, 'shadowColor')
|
|
445
|
-
allValues.tabsBackgroundColor = combineKeys(configFile.theme, base.colors, 'tabsBackgroundColor')
|
|
446
|
-
allValues.tabsBackgroundDisabledColor = combineKeys(configFile.theme, base.colors, 'tabsBackgroundDisabledColor')
|
|
447
|
-
allValues.tabsBackgroundFocusedColor = combineKeys(configFile.theme, base.colors, 'tabsBackgroundFocusedColor')
|
|
448
|
-
allValues.tabsBackgroundSelectedColor = combineKeys(configFile.theme, base.colors, 'tabsBackgroundSelectedColor')
|
|
449
|
-
allValues.textColor = combineKeys(configFile.theme, base.colors, 'textColor')
|
|
450
|
-
allValues.tintColor = combineKeys(configFile.theme, base.colors, 'tintColor')
|
|
451
|
-
allValues.titleColor = combineKeys(configFile.theme, base.colors, 'titleColor')
|
|
452
|
-
allValues.titleDisabledColor = combineKeys(configFile.theme, base.colors, 'titleDisabledColor')
|
|
453
|
-
allValues.titleFocusedColor = combineKeys(configFile.theme, base.colors, 'titleFocusedColor')
|
|
454
|
-
allValues.titleHighlightedColor = combineKeys(configFile.theme, base.colors, 'titleHighlightedColor')
|
|
455
|
-
allValues.titleSelectedColor = combineKeys(configFile.theme, base.colors, 'titleSelectedColor')
|
|
456
|
-
allValues.touchFeedbackColor = combineKeys(configFile.theme, base.colors, 'touchFeedbackColor')
|
|
457
|
-
allValues.viewShadowColor = combineKeys(configFile.theme, base.colors, 'viewShadowColor')
|
|
458
|
-
allValues.navTintColor = combineKeys(configFile.theme, base.colors, 'navTintColor')
|
|
459
|
-
|
|
460
|
-
return allValues
|
|
461
|
-
}
|
|
@@ -73,6 +73,26 @@ export const IPHONE_SCALES = Object.freeze([
|
|
|
73
73
|
{ suffix: '@3x', factor: 3 / 4 }
|
|
74
74
|
])
|
|
75
75
|
|
|
76
|
+
// Resolve target dimensions for a single scale.
|
|
77
|
+
// `factor * 4` recovers the integer multiplier (1, 1.5, 2, 3, 4 for Android;
|
|
78
|
+
// 1, 2, 3 for iPhone) only because every entry in *_SCALES is normalized to
|
|
79
|
+
// n/4 with the largest scale (xxxhdpi/@4x) at 4/4. If a future density is
|
|
80
|
+
// added beyond xxxhdpi, this conversion factor needs to be revisited.
|
|
81
|
+
function computeScaleTarget(srcMeta, factor, baseWidth) {
|
|
82
|
+
if (baseWidth == null) {
|
|
83
|
+
return {
|
|
84
|
+
targetWidth: Math.max(1, Math.round(srcMeta.width * factor)),
|
|
85
|
+
targetHeight: Math.max(1, Math.round(srcMeta.height * factor))
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const multiplier = factor * 4
|
|
89
|
+
const aspect = srcMeta.width > 0 ? (srcMeta.height / srcMeta.width) : 1
|
|
90
|
+
return {
|
|
91
|
+
targetWidth: Math.max(1, Math.round(baseWidth * multiplier)),
|
|
92
|
+
targetHeight: Math.max(1, Math.round(baseWidth * multiplier * aspect))
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
76
96
|
/**
|
|
77
97
|
* Scale a source image into all Android density variants.
|
|
78
98
|
*
|
|
@@ -85,13 +105,12 @@ export const IPHONE_SCALES = Object.freeze([
|
|
|
85
105
|
* @returns {Promise<string[]>} Paths written
|
|
86
106
|
*/
|
|
87
107
|
export async function genAndroidScales(sourceFile, relPath, androidBaseDir, opts = {}) {
|
|
88
|
-
const { format = null, quality = 85 } = opts
|
|
108
|
+
const { format = null, quality = 85, baseWidth = null } = opts
|
|
89
109
|
const src = await readSource(sourceFile)
|
|
90
110
|
const written = []
|
|
91
111
|
|
|
92
112
|
for (const { name, factor } of ANDROID_SCALES) {
|
|
93
|
-
const targetWidth =
|
|
94
|
-
const targetHeight = Math.max(1, Math.round(src.meta.height * factor))
|
|
113
|
+
const { targetWidth, targetHeight } = computeScaleTarget(src.meta, factor, baseWidth)
|
|
95
114
|
|
|
96
115
|
const outDir = path.join(androidBaseDir, name, path.dirname(relPath))
|
|
97
116
|
fs.mkdirSync(outDir, { recursive: true })
|
|
@@ -113,7 +132,7 @@ export async function genAndroidScales(sourceFile, relPath, androidBaseDir, opts
|
|
|
113
132
|
* @returns {Promise<string[]>} Paths written
|
|
114
133
|
*/
|
|
115
134
|
export async function genIphoneScales(sourceFile, relPath, iphoneBaseDir, opts = {}) {
|
|
116
|
-
const { format = null, quality = 85 } = opts
|
|
135
|
+
const { format = null, quality = 85, baseWidth = null } = opts
|
|
117
136
|
const src = await readSource(sourceFile)
|
|
118
137
|
const written = []
|
|
119
138
|
|
|
@@ -122,8 +141,7 @@ export async function genIphoneScales(sourceFile, relPath, iphoneBaseDir, opts =
|
|
|
122
141
|
fs.mkdirSync(outDir, { recursive: true })
|
|
123
142
|
|
|
124
143
|
for (const { suffix, factor } of IPHONE_SCALES) {
|
|
125
|
-
const targetWidth =
|
|
126
|
-
const targetHeight = Math.max(1, Math.round(src.meta.height * factor))
|
|
144
|
+
const { targetWidth, targetHeight } = computeScaleTarget(src.meta, factor, baseWidth)
|
|
127
145
|
|
|
128
146
|
// SVG sources can't be written as SVG by Sharp — fall back to PNG if the
|
|
129
147
|
// user didn't specify an explicit output format.
|
package/src/core/images/index.js
CHANGED
|
@@ -35,6 +35,7 @@ export async function runImages(opts) {
|
|
|
35
35
|
iphoneOnly = false,
|
|
36
36
|
format = null,
|
|
37
37
|
quality = 85,
|
|
38
|
+
baseWidth = null,
|
|
38
39
|
dryRun = false,
|
|
39
40
|
yes = false,
|
|
40
41
|
confirmOverwrites = true
|
|
@@ -49,6 +50,11 @@ export async function runImages(opts) {
|
|
|
49
50
|
|
|
50
51
|
const files = collectImageFiles(source)
|
|
51
52
|
|
|
53
|
+
if (baseWidth == null && files.some(f => path.extname(f).toLowerCase() === '.svg')) {
|
|
54
|
+
logger.warning('⚠ SVG source detected without --width. Output sizes will be derived from each SVG\'s viewBox (treated as a 4× master).')
|
|
55
|
+
logger.warning(' For SVGs from vector editors with disproportionate viewBoxes, pass --width <n> (e.g. --width 256) to pin the @1x/mdpi width.')
|
|
56
|
+
}
|
|
57
|
+
|
|
52
58
|
console.log()
|
|
53
59
|
mainLogger.info('Generating multi-density image variants...')
|
|
54
60
|
console.log()
|
|
@@ -60,6 +66,7 @@ export async function runImages(opts) {
|
|
|
60
66
|
if (!androidOnly) platforms.push('iPhone (@1x, @2x, @3x)')
|
|
61
67
|
logger.property('Platforms: ', platforms.join(' + '))
|
|
62
68
|
if (format) logger.property('Format: ', `convert all to ${format}`)
|
|
69
|
+
if (baseWidth != null) logger.property('Width: ', `${baseWidth} px @1x/mdpi`)
|
|
63
70
|
if (dryRun) logger.warning('DRY RUN — no files will be written')
|
|
64
71
|
|
|
65
72
|
if (files.length === 0) {
|
|
@@ -115,11 +122,11 @@ export async function runImages(opts) {
|
|
|
115
122
|
if (dryRun) continue
|
|
116
123
|
|
|
117
124
|
if (!iphoneOnly) {
|
|
118
|
-
const androidFiles = await genAndroidScales(file, relPath, androidBaseDir, { format, quality })
|
|
125
|
+
const androidFiles = await genAndroidScales(file, relPath, androidBaseDir, { format, quality, baseWidth })
|
|
119
126
|
written.push(...androidFiles)
|
|
120
127
|
}
|
|
121
128
|
if (!androidOnly) {
|
|
122
|
-
const iphoneFiles = await genIphoneScales(file, relPath, iphoneBaseDir, { format, quality })
|
|
129
|
+
const iphoneFiles = await genIphoneScales(file, relPath, iphoneBaseDir, { format, quality, baseWidth })
|
|
123
130
|
written.push(...iphoneFiles)
|
|
124
131
|
}
|
|
125
132
|
}
|
|
@@ -14,6 +14,7 @@ import _ from 'lodash'
|
|
|
14
14
|
import chalk from 'chalk'
|
|
15
15
|
import * as helpers from '../../shared/helpers.js'
|
|
16
16
|
import { logger } from '../../shared/logger.js'
|
|
17
|
+
import { deriveAlphaKey } from '../../shared/semantic-helpers.js'
|
|
17
18
|
import {
|
|
18
19
|
// eslint-disable-next-line camelcase
|
|
19
20
|
projectsTailwind_TSS,
|
|
@@ -191,10 +192,20 @@ export function purgeTailwind(uniqueClasses, debug = false) {
|
|
|
191
192
|
const opacityIndex = _.findIndex(tailwindClasses, line => line.startsWith(`'.${opacityValue.className}'`))
|
|
192
193
|
|
|
193
194
|
const classProperties = tailwindClasses[opacityIndex]
|
|
194
|
-
if (opacityIndex > -1 && classProperties.includes('#')) {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
195
|
+
if (opacityIndex > -1 && classProperties && !classProperties.includes('#')) {
|
|
196
|
+
const derivedLine = tryDeriveSemanticOpacityLine(classProperties, opacityValue)
|
|
197
|
+
if (derivedLine) {
|
|
198
|
+
purgedClasses += switchPlatform(helpers.checkPlatformAndDevice(derivedLine, opacityValue.classNameWithTransparency))
|
|
199
|
+
} else {
|
|
200
|
+
console.warn('')
|
|
201
|
+
console.warn(chalk.yellow(` Skipping ".${opacityValue.className}/${opacityValue.decimalValue}" — semantic color, no hex to blend.`))
|
|
202
|
+
console.warn(chalk.yellow(` Use a PurgeTSS built-in color, bg-(#AARRGGBB), or "purgetss semantic --single ... --alpha ${opacityValue.decimalValue}".`))
|
|
203
|
+
console.warn('')
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (opacityIndex > -1 && classProperties && classProperties.includes('#')) {
|
|
207
|
+
const hexMatches = classProperties.match(/#[0-9a-f]{6}/gi)
|
|
208
|
+
const defaultHexValue = (classProperties.includes('from')) ? hexMatches[1] : hexMatches[0]
|
|
198
209
|
let classWithoutDecimalOpacity = `${classProperties.replace(new RegExp(defaultHexValue.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), `#${opacityValue.transparency}${defaultHexValue.substring(1)}`)}`
|
|
199
210
|
// Special case: #000000
|
|
200
211
|
if (classProperties.includes('from') && defaultHexValue === '#000000') classWithoutDecimalOpacity = classWithoutDecimalOpacity.replace('00000000', '000000')
|
|
@@ -216,6 +227,31 @@ export function purgeTailwind(uniqueClasses, debug = false) {
|
|
|
216
227
|
return purgedClasses
|
|
217
228
|
}
|
|
218
229
|
|
|
230
|
+
// Auto-derive a semantic key with applied alpha and emit a TSS line for the
|
|
231
|
+
// `class/N` form. Returns the rewritten line (with selector renamed to include
|
|
232
|
+
// `/N` and the semantic value swapped for the derived key), or `null` when no
|
|
233
|
+
// candidate matches an entry in semantic.colors.json. Conflict errors from
|
|
234
|
+
// `deriveAlphaKey` propagate naturally.
|
|
235
|
+
function tryDeriveSemanticOpacityLine(classProperties, opacityValue) {
|
|
236
|
+
const bodyMatch = classProperties.match(/\{([^}]*)\}/)
|
|
237
|
+
if (!bodyMatch) return null
|
|
238
|
+
const candidates = (bodyMatch[1].match(/'([^']+)'/g) || [])
|
|
239
|
+
.map(m => m.slice(1, -1))
|
|
240
|
+
.filter(v => !v.startsWith('#'))
|
|
241
|
+
for (const candidate of candidates) {
|
|
242
|
+
const derivedKey = deriveAlphaKey(candidate, opacityValue.decimalValue)
|
|
243
|
+
if (derivedKey) {
|
|
244
|
+
let line = classProperties.replace(new RegExp(`'${candidate}'`, 'g'), `'${derivedKey}'`)
|
|
245
|
+
line = line.replace(
|
|
246
|
+
`'.${opacityValue.className}'`,
|
|
247
|
+
`'.${opacityValue.className}/${opacityValue.decimalValue}'`
|
|
248
|
+
)
|
|
249
|
+
return line
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return null
|
|
253
|
+
}
|
|
254
|
+
|
|
219
255
|
/**
|
|
220
256
|
* Switch platform specific styles - COPIED exactly from original switchPlatform() function
|
|
221
257
|
* NO CHANGES to logic, preserving 100% of original functionality
|
|
@@ -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
|
-
}
|