UNPKG

12.2 kBJavaScriptView Raw
1import * as util from '../util';
2import * as is from '../is';
3import * as math from '../math';
4
5let styfn = {};
6
7// a caching layer for property parsing
8styfn.parse = function( name, value, propIsBypass, propIsFlat ){
9 let self = this;
10
11 // function values can't be cached in all cases, and there isn't much benefit of caching them anyway
12 if( is.fn( value ) ){
13 return self.parseImplWarn( name, value, propIsBypass, propIsFlat );
14 }
15
16 let flatKey = ( propIsFlat === 'mapping' || propIsFlat === true || propIsFlat === false || propIsFlat == null ) ? 'dontcare' : propIsFlat;
17 let bypassKey = propIsBypass ? 't' : 'f';
18 let valueKey = '' + value;
19 let argHash = util.hashStrings( name, valueKey, bypassKey, flatKey );
20 let propCache = self.propCache = self.propCache || [];
21 let ret;
22
23 if( !(ret = propCache[ argHash ]) ){
24 ret = propCache[ argHash ] = self.parseImplWarn( name, value, propIsBypass, propIsFlat );
25 }
26
27 // - bypasses can't be shared b/c the value can be changed by animations or otherwise overridden
28 // - mappings can't be shared b/c mappings are per-element
29 if( propIsBypass || propIsFlat === 'mapping' ){
30 // need a copy since props are mutated later in their lifecycles
31 ret = util.copy( ret );
32
33 if( ret ){
34 ret.value = util.copy( ret.value ); // because it could be an array, e.g. colour
35 }
36 }
37
38 return ret;
39};
40
41styfn.parseImplWarn = function( name, value, propIsBypass, propIsFlat ){
42 let prop = this.parseImpl( name, value, propIsBypass, propIsFlat );
43
44 if( !prop && value != null ){
45 util.warn(`The style property \`${name}: ${value}\` is invalid`);
46 }
47
48 if( prop && (prop.name === 'width' || prop.name === 'height') && value === 'label' ){
49 util.warn('The style value of `label` is deprecated for `' + prop.name + '`');
50 }
51
52 return prop;
53};
54
55// parse a property; return null on invalid; return parsed property otherwise
56// fields :
57// - name : the name of the property
58// - value : the parsed, native-typed value of the property
59// - strValue : a string value that represents the property value in valid css
60// - bypass : true iff the property is a bypass property
61styfn.parseImpl = function( name, value, propIsBypass, propIsFlat ){
62 let self = this;
63
64 name = util.camel2dash( name ); // make sure the property name is in dash form (e.g. 'property-name' not 'propertyName')
65
66 let property = self.properties[ name ];
67 let passedValue = value;
68 let types = self.types;
69
70 if( !property ){ return null; } // return null on property of unknown name
71 if( value === undefined ){ return null; } // can't assign undefined
72
73 // the property may be an alias
74 if( property.alias ){
75 property = property.pointsTo;
76 name = property.name;
77 }
78
79 let valueIsString = is.string( value );
80 if( valueIsString ){ // trim the value to make parsing easier
81 value = value.trim();
82 }
83
84 let type = property.type;
85 if( !type ){ return null; } // no type, no luck
86
87 // check if bypass is null or empty string (i.e. indication to delete bypass property)
88 if( propIsBypass && (value === '' || value === null) ){
89 return {
90 name: name,
91 value: value,
92 bypass: true,
93 deleteBypass: true
94 };
95 }
96
97 // check if value is a function used as a mapper
98 if( is.fn( value ) ){
99 return {
100 name: name,
101 value: value,
102 strValue: 'fn',
103 mapped: types.fn,
104 bypass: propIsBypass
105 };
106 }
107
108 // check if value is mapped
109 let data, mapData;
110 if( !valueIsString || propIsFlat || value.length < 7 || value[1] !== 'a' ){
111 // then don't bother to do the expensive regex checks
112
113 } else if(value.length >= 7 && value[0] === 'd' && ( data = new RegExp( types.data.regex ).exec( value ) )){
114 if( propIsBypass ){ return false; } // mappers not allowed in bypass
115
116 let mapped = types.data;
117
118 return {
119 name: name,
120 value: data,
121 strValue: '' + value,
122 mapped: mapped,
123 field: data[1],
124 bypass: propIsBypass
125 };
126
127 } else if(value.length >= 10 && value[0] === 'm' && ( mapData = new RegExp( types.mapData.regex ).exec( value ) )){
128 if( propIsBypass ){ return false; } // mappers not allowed in bypass
129 if( type.multiple ){ return false; } // impossible to map to num
130
131 let mapped = types.mapData;
132
133 // we can map only if the type is a colour or a number
134 if( !(type.color || type.number) ){ return false; }
135
136 let valueMin = this.parse( name, mapData[4] ); // parse to validate
137 if( !valueMin || valueMin.mapped ){ return false; } // can't be invalid or mapped
138
139 let valueMax = this.parse( name, mapData[5] ); // parse to validate
140 if( !valueMax || valueMax.mapped ){ return false; } // can't be invalid or mapped
141
142 // check if valueMin and valueMax are the same
143 if( valueMin.pfValue === valueMax.pfValue || valueMin.strValue === valueMax.strValue ){
144 util.warn('`' + name + ': ' + value + '` is not a valid mapper because the output range is zero; converting to `' + name + ': ' + valueMin.strValue + '`');
145
146 return this.parse(name, valueMin.strValue); // can't make much of a mapper without a range
147
148 } else if( type.color ){
149 let c1 = valueMin.value;
150 let c2 = valueMax.value;
151
152 let same = c1[0] === c2[0] // red
153 && c1[1] === c2[1] // green
154 && c1[2] === c2[2] // blue
155 && ( // optional alpha
156 c1[3] === c2[3] // same alpha outright
157 || (
158 (c1[3] == null || c1[3] === 1) // full opacity for colour 1?
159 &&
160 (c2[3] == null || c2[3] === 1) // full opacity for colour 2?
161 )
162 )
163 ;
164
165 if( same ){ return false; } // can't make a mapper without a range
166 }
167
168 return {
169 name: name,
170 value: mapData,
171 strValue: '' + value,
172 mapped: mapped,
173 field: mapData[1],
174 fieldMin: parseFloat( mapData[2] ), // min & max are numeric
175 fieldMax: parseFloat( mapData[3] ),
176 valueMin: valueMin.value,
177 valueMax: valueMax.value,
178 bypass: propIsBypass
179 };
180 }
181
182 if( type.multiple && propIsFlat !== 'multiple' ){
183 let vals;
184
185 if( valueIsString ){
186 vals = value.split( /\s+/ );
187 } else if( is.array( value ) ){
188 vals = value;
189 } else {
190 vals = [ value ];
191 }
192
193 if( type.evenMultiple && vals.length % 2 !== 0 ){ return null; }
194
195 let valArr = [];
196 let unitsArr = [];
197 let pfValArr = [];
198 let strVal = '';
199 let hasEnum = false;
200
201 for( let i = 0; i < vals.length; i++ ){
202 let p = self.parse( name, vals[i], propIsBypass, 'multiple' );
203
204 hasEnum = hasEnum || is.string( p.value );
205
206 valArr.push( p.value );
207 pfValArr.push( p.pfValue != null ? p.pfValue : p.value );
208 unitsArr.push( p.units );
209 strVal += (i > 0 ? ' ' : '') + p.strValue;
210 }
211
212 if( type.validate && !type.validate( valArr, unitsArr ) ){
213 return null;
214 }
215
216 if( type.singleEnum && hasEnum ){
217 if( valArr.length === 1 && is.string( valArr[0] ) ){
218 return {
219 name: name,
220 value: valArr[0],
221 strValue: valArr[0],
222 bypass: propIsBypass
223 };
224 } else {
225 return null;
226 }
227 }
228
229 return {
230 name: name,
231 value: valArr,
232 pfValue: pfValArr,
233 strValue: strVal,
234 bypass: propIsBypass,
235 units: unitsArr
236 };
237 }
238
239 // several types also allow enums
240 let checkEnums = function(){
241 for( let i = 0; i < type.enums.length; i++ ){
242 let en = type.enums[ i ];
243
244 if( en === value ){
245 return {
246 name: name,
247 value: value,
248 strValue: '' + value,
249 bypass: propIsBypass
250 };
251 }
252 }
253
254 return null;
255 };
256
257 // check the type and return the appropriate object
258 if( type.number ){
259 let units;
260 let implicitUnits = 'px'; // not set => px
261
262 if( type.units ){ // use specified units if set
263 units = type.units;
264 }
265
266 if( type.implicitUnits ){
267 implicitUnits = type.implicitUnits;
268 }
269
270 if( !type.unitless ){
271 if( valueIsString ){
272 let unitsRegex = 'px|em' + (type.allowPercent ? '|\\%' : '');
273 if( units ){ unitsRegex = units; } // only allow explicit units if so set
274 let match = value.match( '^(' + util.regex.number + ')(' + unitsRegex + ')?' + '$' );
275
276 if( match ){
277 value = match[1];
278 units = match[2] || implicitUnits;
279 }
280
281 } else if( !units || type.implicitUnits ){
282 units = implicitUnits; // implicitly px if unspecified
283 }
284 }
285
286 value = parseFloat( value );
287
288 // if not a number and enums not allowed, then the value is invalid
289 if( isNaN( value ) && type.enums === undefined ){
290 return null;
291 }
292
293 // check if this number type also accepts special keywords in place of numbers
294 // (i.e. `left`, `auto`, etc)
295 if( isNaN( value ) && type.enums !== undefined ){
296 value = passedValue;
297
298 return checkEnums();
299 }
300
301 // check if value must be an integer
302 if( type.integer && !is.integer( value ) ){
303 return null;
304 }
305
306 // check value is within range
307 if( ( type.min !== undefined && ( value < type.min || (type.strictMin && value === type.min) ) )
308 || ( type.max !== undefined && ( value > type.max || (type.strictMax && value === type.max) ) )
309 ){
310 return null;
311 }
312
313 let ret = {
314 name: name,
315 value: value,
316 strValue: '' + value + (units ? units : ''),
317 units: units,
318 bypass: propIsBypass
319 };
320
321 // normalise value in pixels
322 if( type.unitless || (units !== 'px' && units !== 'em') ){
323 ret.pfValue = value;
324 } else {
325 ret.pfValue = ( units === 'px' || !units ? (value) : (this.getEmSizeInPixels() * value) );
326 }
327
328 // normalise value in ms
329 if( units === 'ms' || units === 's' ){
330 ret.pfValue = units === 'ms' ? value : 1000 * value;
331 }
332
333 // normalise value in rad
334 if( units === 'deg' || units === 'rad' ){
335 ret.pfValue = units === 'rad' ? value : math.deg2rad( value );
336 }
337
338 // normalize value in %
339 if( units === '%' ){
340 ret.pfValue = value / 100;
341 }
342
343 return ret;
344
345 } else if( type.propList ){
346
347 let props = [];
348 let propsStr = '' + value;
349
350 if( propsStr === 'none' ){
351 // leave empty
352
353 } else { // go over each prop
354
355 let propsSplit = propsStr.split( /\s*,\s*|\s+/ );
356 for( let i = 0; i < propsSplit.length; i++ ){
357 let propName = propsSplit[ i ].trim();
358
359 if( self.properties[ propName ] ){
360 props.push( propName );
361 } else {
362 util.warn('`' + propName + '` is not a valid property name');
363 }
364 }
365
366 if( props.length === 0 ){ return null; }
367 }
368
369 return {
370 name: name,
371 value: props,
372 strValue: props.length === 0 ? 'none' : props.join(' '),
373 bypass: propIsBypass
374 };
375
376 } else if( type.color ){
377 let tuple = util.color2tuple( value );
378
379 if( !tuple ){ return null; }
380
381 return {
382 name: name,
383 value: tuple,
384 pfValue: tuple,
385 strValue: 'rgb(' + tuple[0] + ',' + tuple[1] + ',' + tuple[2] + ')', // n.b. no spaces b/c of multiple support
386 bypass: propIsBypass
387 };
388
389 } else if( type.regex || type.regexes ){
390
391 // first check enums
392 if( type.enums ){
393 let enumProp = checkEnums();
394
395 if( enumProp ){ return enumProp; }
396 }
397
398 let regexes = type.regexes ? type.regexes : [ type.regex ];
399
400 for( let i = 0; i < regexes.length; i++ ){
401 let regex = new RegExp( regexes[ i ] ); // make a regex from the type string
402 let m = regex.exec( value );
403
404 if( m ){ // regex matches
405 return {
406 name: name,
407 value: type.singleRegexMatchValue ? m[1] : m,
408 strValue: '' + value,
409 bypass: propIsBypass
410 };
411
412 }
413 }
414
415 return null; // didn't match any
416
417 } else if( type.string ){
418 // just return
419 return {
420 name: name,
421 value: '' + value,
422 strValue: '' + value,
423 bypass: propIsBypass
424 };
425
426 } else if( type.enums ){ // check enums last because it's a combo type in others
427 return checkEnums();
428
429 } else {
430 return null; // not a type we can handle
431 }
432
433};
434
435export default styfn;