Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Download
81141 views
1
/*********************************************************************
2
* These are commonly used parsers for CSS Values they take a string *
3
* to parse and return a string after it's been converted, if needed *
4
********************************************************************/
5
'use strict';
6
7
exports.TYPES = {
8
INTEGER: 1,
9
NUMBER: 2,
10
LENGTH: 3,
11
PERCENT: 4,
12
URL: 5,
13
COLOR: 6,
14
STRING: 7,
15
ANGLE: 8,
16
KEYWORD: 9,
17
NULL_OR_EMPTY_STR: 10
18
};
19
20
/*jslint regexp: true*/
21
// rough regular expressions
22
var integerRegEx = /^[\-+]?[0-9]+$/;
23
var numberRegEx = /^[\-+]?[0-9]*\.[0-9]+$/;
24
var lengthRegEx = /^(0|[\-+]?[0-9]*\.?[0-9]+(in|cm|em|mm|pt|pc|px))$/;
25
var percentRegEx = /^[\-+]?[0-9]*\.?[0-9]+%$/;
26
var urlRegEx = /^url\(\s*([^\)]*)\s*\)$/;
27
var stringRegEx = /^("[^"]*"|'[^']*')$/;
28
var colorRegEx1 = /^#[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]([0-9a-fA-F][0-9a-fA-F][0-9a-fA-F])?$/;
29
var colorRegEx2 = /^rgb\(([^\)]*)\)$/;
30
var colorRegEx3 = /^rgba\(([^\)]*)\)$/;
31
var angleRegEx = /^([\-+]?[0-9]*\.?[0-9]+)(deg|grad|rad)$/;
32
/*jslint regexp: false*/
33
34
// This will return one of the above types based on the passed in string
35
exports.valueType = function valueType(val) {
36
if (val === '' || val === null) {
37
return exports.TYPES.NULL_OR_EMPTY_STR;
38
}
39
if (typeof val === 'number') {
40
val = val.toString();
41
}
42
43
if (typeof val !== 'string') {
44
return undefined;
45
}
46
47
if (integerRegEx.test(val)) {
48
return exports.TYPES.INTEGER;
49
}
50
if (numberRegEx.test(val)) {
51
return exports.TYPES.NUMBER;
52
}
53
if (lengthRegEx.test(val)) {
54
return exports.TYPES.LENGTH;
55
}
56
if (percentRegEx.test(val)) {
57
return exports.TYPES.PERCENT;
58
}
59
if (urlRegEx.test(val)) {
60
return exports.TYPES.URL;
61
}
62
if (stringRegEx.test(val)) {
63
return exports.TYPES.STRING;
64
}
65
if (angleRegEx.test(val)) {
66
return exports.TYPES.ANGLE;
67
}
68
if (colorRegEx1.test(val)) {
69
return exports.TYPES.COLOR;
70
}
71
var res = colorRegEx2.exec(val);
72
var parts;
73
if (res !== null) {
74
parts = res[1].split(/\s*,\s*/);
75
if (parts.length !== 3) {
76
return undefined;
77
}
78
if (parts.every(percentRegEx.test.bind(percentRegEx)) || parts.every(integerRegEx.test.bind(integerRegEx))) {
79
return exports.TYPES.COLOR;
80
}
81
return undefined;
82
}
83
res = colorRegEx3.exec(val);
84
if (res !== null) {
85
parts = res[1].split(/\s*,\s*/);
86
if (parts.length !== 4) {
87
return undefined;
88
}
89
if (parts.slice(0, 3).every(percentRegEx.test.bind(percentRegEx)) || parts.every(integerRegEx.test.bind(integerRegEx))) {
90
if (numberRegEx.test(parts[3])) {
91
return exports.TYPES.COLOR;
92
}
93
}
94
return undefined;
95
}
96
97
// could still be a color, one of the standard keyword colors
98
val = val.toLowerCase();
99
switch (val) {
100
case 'maroon':
101
case 'red':
102
case 'orange':
103
case 'yellow':
104
case 'olive':
105
case 'purple':
106
case 'fuchsia':
107
case 'white':
108
case 'lime':
109
case 'green':
110
case 'navy':
111
case 'blue':
112
case 'aqua':
113
case 'teal':
114
case 'black':
115
case 'silver':
116
case 'gray':
117
// the following are deprecated in CSS3
118
case 'activeborder':
119
case 'activecaption':
120
case 'appworkspace':
121
case 'background':
122
case 'buttonface':
123
case 'buttonhighlight':
124
case 'buttonshadow':
125
case 'buttontext':
126
case 'captiontext':
127
case 'graytext':
128
case 'highlight':
129
case 'highlighttext':
130
case 'inactiveborder':
131
case 'inactivecaption':
132
case 'inactivecaptiontext':
133
case 'infobackground':
134
case 'infotext':
135
case 'menu':
136
case 'menutext':
137
case 'scrollbar':
138
case 'threeddarkshadow':
139
case 'threedface':
140
case 'threedhighlight':
141
case 'threedlightshadow':
142
case 'threedshadow':
143
case 'window':
144
case 'windowframe':
145
case 'windowtext':
146
return exports.TYPES.COLOR;
147
default:
148
return exports.TYPES.KEYWORD;
149
}
150
};
151
152
exports.parseInteger = function parseInteger(val) {
153
var type = exports.valueType(val);
154
if (type === exports.TYPES.NULL_OR_EMPTY_STR) {
155
return val;
156
}
157
if (type !== exports.TYPES.INTEGER) {
158
return undefined;
159
}
160
return String(parseInt(val, 10));
161
};
162
163
exports.parseNumber = function parseNumber(val) {
164
var type = exports.valueType(val);
165
if (type === exports.TYPES.NULL_OR_EMPTY_STR) {
166
return val;
167
}
168
if (type !== exports.TYPES.NUMBER && type !== exports.TYPES.INTEGER) {
169
return undefined;
170
}
171
return String(parseFloat(val));
172
};
173
174
exports.parseLength = function parseLength(val) {
175
if (val === 0 || val === '0') {
176
return '0px';
177
}
178
var type = exports.valueType(val);
179
if (type === exports.TYPES.NULL_OR_EMPTY_STR) {
180
return val;
181
}
182
if (type !== exports.TYPES.LENGTH) {
183
return undefined;
184
}
185
return val;
186
};
187
188
exports.parsePercent = function parsePercent(val) {
189
if (val === 0 || val === '0') {
190
return '0%';
191
}
192
var type = exports.valueType(val);
193
if (type === exports.TYPES.NULL_OR_EMPTY_STR) {
194
return val;
195
}
196
if (type !== exports.TYPES.PERCENT) {
197
return undefined;
198
}
199
return val;
200
};
201
202
// either a length or a percent
203
exports.parseMeasurement = function parseMeasurement(val) {
204
var length = exports.parseLength(val);
205
if (length !== undefined) {
206
return length;
207
}
208
return exports.parsePercent(val);
209
};
210
211
exports.parseUrl = function parseUrl(val) {
212
var type = exports.valueType(val);
213
if (type === exports.TYPES.NULL_OR_EMPTY_STR) {
214
return val;
215
}
216
var res = urlRegEx.exec(val);
217
// does it match the regex?
218
if (!res) {
219
return undefined;
220
}
221
var str = res[1];
222
// if it starts with single or double quotes, does it end with the same?
223
if ((str[1] === '"' || str[1] === "'") && str[1] !== str[str.length - 1]) {
224
return undefined;
225
}
226
if (str[1] === '"' || str[1] === "'") {
227
str = str.substr(1, -1);
228
}
229
230
var i;
231
for (i = 0; i < str.length; i++) {
232
switch (str[i]) {
233
case '(':
234
case ')':
235
case ' ':
236
case '\t':
237
case '\n':
238
case "'":
239
case '"':
240
return undefined;
241
case '\\':
242
i++;
243
break;
244
}
245
}
246
return 'url(' + str + ')';
247
};
248
249
exports.parseString = function parseString(val) {
250
var type = exports.valueType(val);
251
if (type === exports.TYPES.NULL_OR_EMPTY_STR) {
252
return val;
253
}
254
if (type !== exports.TYPES.STRING) {
255
return undefined;
256
}
257
var i;
258
for (i = 1; i < val.length - 1; i++) {
259
switch (val[i]) {
260
case val[0]:
261
return undefined;
262
case '\\':
263
i++;
264
while (i < val.length - 1 && /[0-9A-Fa-f]/.test(val[i])) {
265
i++;
266
}
267
break;
268
}
269
}
270
if (i >= val.length) {
271
return undefined;
272
}
273
return val;
274
};
275
276
exports.parseColor = function parseColor(val) {
277
var type = exports.valueType(val);
278
if (type === exports.TYPES.NULL_OR_EMPTY_STR) {
279
return val;
280
}
281
var red, green, blue, alpha = 1;
282
var parts;
283
var res = colorRegEx1.exec(val);
284
// is it #aaa or #ababab
285
if (res) {
286
var hex = val.substr(1);
287
if (hex.length === 3) {
288
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
289
}
290
red = parseInt(hex.substr(0, 2), 16);
291
green = parseInt(hex.substr(2, 2), 16);
292
blue = parseInt(hex.substr(4, 2), 16);
293
return 'rgb(' + red + ', ' + green + ', ' + blue + ')';
294
}
295
296
res = colorRegEx2.exec(val);
297
if (res) {
298
parts = res[1].split(/\s*,\s*/);
299
if (parts.length !== 3) {
300
return undefined;
301
}
302
if (parts.every(percentRegEx.test.bind(percentRegEx))) {
303
red = Math.floor(parseFloat(parts[0].slice(0, -1)) * 255 / 100);
304
green = Math.floor(parseFloat(parts[1].slice(0, -1)) * 255 / 100);
305
blue = Math.floor(parseFloat(parts[2].slice(0, -1)) * 255 / 100);
306
} else if (parts.every(integerRegEx.test.bind(integerRegEx))) {
307
red = parseInt(parts[0], 10);
308
green = parseInt(parts[1], 10);
309
blue = parseInt(parts[2], 10);
310
} else {
311
return undefined;
312
}
313
red = Math.min(255, Math.max(0, red));
314
green = Math.min(255, Math.max(0, green));
315
blue = Math.min(255, Math.max(0, blue));
316
return 'rgb(' + red + ', ' + green + ', ' + blue + ')';
317
}
318
319
res = colorRegEx3.exec(val);
320
if (res) {
321
parts = res[1].split(/\s*,\s*/);
322
if (parts.length !== 4) {
323
return undefined;
324
}
325
if (parts.slice(0, 3).every(percentRegEx.test.bind(percentRegEx))) {
326
red = Math.floor(parseFloat(parts[0].slice(0, -1)) * 255 / 100);
327
green = Math.floor(parseFloat(parts[1].slice(0, -1)) * 255 / 100);
328
blue = Math.floor(parseFloat(parts[2].slice(0, -1)) * 255 / 100);
329
alpha = parseFloat(parts[3]);
330
} else if (parts.slice(0, 3).every(integerRegEx.test.bind(integerRegEx))) {
331
red = parseInt(parts[0], 10);
332
green = parseInt(parts[1], 10);
333
blue = parseInt(parts[2], 10);
334
alpha = parseFloat(parts[3]);
335
} else {
336
return undefined;
337
}
338
if (isNaN(alpha)) {
339
alpha = 1;
340
}
341
red = Math.min(255, Math.max(0, red));
342
green = Math.min(255, Math.max(0, green));
343
blue = Math.min(255, Math.max(0, blue));
344
alpha = Math.min(1, Math.max(0, alpha));
345
if (alpha === 1) {
346
return 'rgb(' + red + ', ' + green + ', ' + blue + ')';
347
}
348
return 'rgba(' + red + ', ' + green + ', ' + blue + ', ' + alpha + ')';
349
}
350
351
if (type === exports.TYPES.COLOR) {
352
return val;
353
}
354
return undefined;
355
};
356
357
exports.parseAngle = function parseAngle(val) {
358
var type = exports.valueType(val);
359
if (type === exports.TYPES.NULL_OR_EMPTY_STR) {
360
return val;
361
}
362
if (type !== exports.TYPES.ANGLE) {
363
return undefined;
364
}
365
var res = angleRegEx.exec(val);
366
var flt = parseFloat(res[1]);
367
if (res[2] === 'rad') {
368
flt *= 180 / Math.PI;
369
} else if (res[2] === 'grad') {
370
flt *= 360 / 400;
371
}
372
373
while (flt < 0) {
374
flt += 360;
375
}
376
while (flt > 360) {
377
flt -= 360;
378
}
379
return flt + 'deg';
380
};
381
382
exports.parseKeyword = function parseKeyword(val, valid_keywords) {
383
var type = exports.valueType(val);
384
if (type === exports.TYPES.NULL_OR_EMPTY_STR) {
385
return val;
386
}
387
if (type !== exports.TYPES.KEYWORD) {
388
return undefined;
389
}
390
val = val.toString().toLowerCase();
391
var i;
392
for (i = 0; i < valid_keywords.length; i++) {
393
if (valid_keywords[i].toLowerCase() === val) {
394
return valid_keywords[i];
395
}
396
}
397
return undefined;
398
};
399
400
// utility to translate from border-width to borderWidth
401
var dashedToCamelCase = function (dashed) {
402
var i;
403
var camel = '';
404
var nextCap = false;
405
for (i = 0; i < dashed.length; i++) {
406
if (dashed[i] !== '-') {
407
camel += nextCap ? dashed[i].toUpperCase() : dashed[i];
408
nextCap = false;
409
} else {
410
nextCap = true;
411
}
412
}
413
return camel;
414
};
415
exports.dashedToCamelCase = dashedToCamelCase;
416
417
var is_space = /\s/;
418
var opening_deliminators = ['"', '\'', '('];
419
var closing_deliminators = ['"', '\'', ')'];
420
// this splits on whitespace, but keeps quoted and parened parts together
421
var getParts = function (str) {
422
var deliminator_stack = [];
423
var length = str.length;
424
var i;
425
var parts = [];
426
var current_part = '';
427
var opening_index;
428
var closing_index;
429
for (i = 0; i < length; i++) {
430
opening_index = opening_deliminators.indexOf(str[i]);
431
closing_index = closing_deliminators.indexOf(str[i]);
432
if (is_space.test(str[i])) {
433
if (deliminator_stack.length === 0) {
434
parts.push(current_part);
435
current_part = '';
436
} else {
437
current_part += str[i];
438
}
439
} else {
440
if (str[i] === '\\') {
441
i++;
442
current_part += str[i];
443
} else {
444
current_part += str[i];
445
if (closing_index !== -1 && closing_index === deliminator_stack[deliminator_stack.length - 1]) {
446
deliminator_stack.pop();
447
} else if (opening_index !== -1) {
448
deliminator_stack.push(opening_index);
449
}
450
}
451
}
452
}
453
if (current_part !== '') {
454
parts.push(current_part);
455
}
456
return parts;
457
};
458
459
/*
460
* this either returns undefined meaning that it isn't valid
461
* or returns an object where the keys are dashed short
462
* hand properties and the values are the values to set
463
* on them
464
*/
465
exports.shorthandParser = function parse(v, shorthand_for) {
466
var obj = {};
467
var type = exports.valueType(v);
468
if (type === exports.TYPES.NULL_OR_EMPTY_STR) {
469
Object.keys(shorthand_for).forEach(function (property) {
470
obj[property] = v;
471
});
472
return obj;
473
}
474
475
if (typeof v === 'number') {
476
v = v.toString();
477
}
478
479
if (typeof v !== 'string') {
480
return undefined;
481
}
482
483
if (v.toLowerCase() === 'inherit') {
484
return {};
485
}
486
var parts = getParts(v);
487
var valid = true;
488
parts.forEach(function (part) {
489
var part_valid = false;
490
Object.keys(shorthand_for).forEach(function (property) {
491
if (shorthand_for[property].isValid(part)) {
492
part_valid = true;
493
obj[property] = part;
494
}
495
});
496
valid = valid && part_valid;
497
});
498
if (!valid) {
499
return undefined;
500
}
501
return obj;
502
};
503
504
exports.shorthandSetter = function (property, shorthand_for) {
505
return function (v) {
506
var obj = exports.shorthandParser(v, shorthand_for);
507
if (obj === undefined) {
508
return;
509
}
510
//console.log('shorthandSetter for:', property, 'obj:', obj);
511
Object.keys(obj).forEach(function (subprop) {
512
// in case subprop is an implicit property, this will clear
513
// *its* subpropertiesX
514
var camel = dashedToCamelCase(subprop);
515
this[camel] = obj[subprop];
516
// in case it gets translated into something else (0 -> 0px)
517
obj[subprop] = this[camel];
518
this.removeProperty(subprop);
519
this._values[subprop] = obj[subprop];
520
}, this);
521
Object.keys(shorthand_for).forEach(function (subprop) {
522
if (!obj.hasOwnProperty(subprop)) {
523
this.removeProperty(subprop);
524
delete this._values[subprop];
525
}
526
}, this);
527
// in case the value is something like 'none' that removes all values,
528
// check that the generated one is not empty, first remove the property
529
// if it already exists, then call the shorthandGetter, if it's an empty
530
// string, don't set the property
531
this.removeProperty(property);
532
var calculated = exports.shorthandGetter(property, shorthand_for).call(this);
533
if (calculated !== '') {
534
this._setProperty(property, calculated);
535
}
536
};
537
};
538
539
exports.shorthandGetter = function (property, shorthand_for) {
540
return function () {
541
if (this._values[property] !== undefined) {
542
return this.getPropertyValue(property);
543
}
544
return Object.keys(shorthand_for).map(function (subprop) {
545
return this.getPropertyValue(subprop);
546
}, this).filter(function (value) {
547
return value !== '';
548
}).join(' ');
549
};
550
};
551
552
// isValid(){1,4} | inherit
553
// if one, it applies to all
554
// if two, the first applies to the top and bottom, and the second to left and right
555
// if three, the first applies to the top, the second to left and right, the third bottom
556
// if four, top, right, bottom, left
557
exports.implicitSetter = function (property_before, property_after, isValid, parser) {
558
property_after = property_after || '';
559
if (property_after !== '') {
560
property_after = '-' + property_after;
561
}
562
563
return function (v) {
564
if (typeof v === 'number') {
565
v = v.toString();
566
}
567
if (typeof v !== 'string') {
568
return undefined;
569
}
570
var parts;
571
if (v.toLowerCase() === 'inherit' || v === '') {
572
parts = [v];
573
} else {
574
parts = getParts(v);
575
}
576
if (parts.length < 1 || parts.length > 4) {
577
return undefined;
578
}
579
580
if (!parts.every(isValid)) {
581
return undefined;
582
}
583
584
this._setProperty(property_before + property_after, parts.map(function (part) { return parser(part); }).join(' '));
585
586
this.removeProperty(property_before + '-top' + property_after);
587
this.removeProperty(property_before + '-right' + property_after);
588
this.removeProperty(property_before + '-bottom' + property_after);
589
this.removeProperty(property_before + '-left' + property_after);
590
switch (parts.length) {
591
case 1:
592
this._values[property_before + '-top' + property_after] = parser(parts[0]);
593
this._values[property_before + '-right' + property_after] = parser(parts[0]);
594
this._values[property_before + '-bottom' + property_after] = parser(parts[0]);
595
this._values[property_before + '-left' + property_after] = parser(parts[0]);
596
return v;
597
case 2:
598
this._values[property_before + '-top' + property_after] = parser(parts[0]);
599
this._values[property_before + '-right' + property_after] = parser(parts[1]);
600
this._values[property_before + '-bottom' + property_after] = parser(parts[0]);
601
this._values[property_before + '-left' + property_after] = parser(parts[1]);
602
return v;
603
case 3:
604
this._values[property_before + '-top' + property_after] = parser(parts[0]);
605
this._values[property_before + '-right' + property_after] = parser(parts[1]);
606
this._values[property_before + '-bottom' + property_after] = parser(parts[2]);
607
this._values[property_before + '-left' + property_after] = parser(parts[1]);
608
return v;
609
case 4:
610
this._values[property_before + '-top' + property_after] = parser(parts[0]);
611
this._values[property_before + '-right' + property_after] = parser(parts[1]);
612
this._values[property_before + '-bottom' + property_after] = parser(parts[2]);
613
this._values[property_before + '-left' + property_after] = parser(parts[3]);
614
return v;
615
}
616
};
617
};
618
619
var camel_to_dashed = /[A-Z]/g;
620
/*jslint regexp: true*/
621
var first_segment = /^\([^\-]\)-/;
622
/*jslint regexp: false*/
623
var vendor_prefixes = ['o', 'moz', 'ms', 'webkit'];
624
exports.camelToDashed = function (camel_case) {
625
var match;
626
var dashed = camel_case.replace(camel_to_dashed, '-$&').toLowerCase();
627
match = dashed.match(first_segment);
628
if (match && vendor_prefixes.indexOf(match[1]) !== -1) {
629
dashed = '-' + dashed;
630
}
631
return dashed;
632
};
633
634