| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722 | /********************************************************************* * These are commonly used parsers for CSS Values they take a string * * to parse and return a string after it's been converted, if needed * ********************************************************************/'use strict';const namedColors = require('./named_colors.json');const { hslToRgb } = require('./utils/colorSpace');exports.TYPES = {  INTEGER: 1,  NUMBER: 2,  LENGTH: 3,  PERCENT: 4,  URL: 5,  COLOR: 6,  STRING: 7,  ANGLE: 8,  KEYWORD: 9,  NULL_OR_EMPTY_STR: 10,  CALC: 11,};// rough regular expressionsvar integerRegEx = /^[-+]?[0-9]+$/;var numberRegEx = /^[-+]?[0-9]*\.?[0-9]+$/;var lengthRegEx = /^(0|[-+]?[0-9]*\.?[0-9]+(in|cm|em|mm|pt|pc|px|ex|rem|vh|vw|ch))$/;var percentRegEx = /^[-+]?[0-9]*\.?[0-9]+%$/;var urlRegEx = /^url\(\s*([^)]*)\s*\)$/;var stringRegEx = /^("[^"]*"|'[^']*')$/;var colorRegEx1 = /^#([0-9a-fA-F]{3,4}){1,2}$/;var colorRegEx2 = /^rgb\(([^)]*)\)$/;var colorRegEx3 = /^rgba\(([^)]*)\)$/;var calcRegEx = /^calc\(([^)]*)\)$/;var colorRegEx4 = /^hsla?\(\s*(-?\d+|-?\d*.\d+)\s*,\s*(-?\d+|-?\d*.\d+)%\s*,\s*(-?\d+|-?\d*.\d+)%\s*(,\s*(-?\d+|-?\d*.\d+)\s*)?\)/;var angleRegEx = /^([-+]?[0-9]*\.?[0-9]+)(deg|grad|rad)$/;// This will return one of the above types based on the passed in stringexports.valueType = function valueType(val) {  if (val === '' || val === null) {    return exports.TYPES.NULL_OR_EMPTY_STR;  }  if (typeof val === 'number') {    val = val.toString();  }  if (typeof val !== 'string') {    return undefined;  }  if (integerRegEx.test(val)) {    return exports.TYPES.INTEGER;  }  if (numberRegEx.test(val)) {    return exports.TYPES.NUMBER;  }  if (lengthRegEx.test(val)) {    return exports.TYPES.LENGTH;  }  if (percentRegEx.test(val)) {    return exports.TYPES.PERCENT;  }  if (urlRegEx.test(val)) {    return exports.TYPES.URL;  }  if (calcRegEx.test(val)) {    return exports.TYPES.CALC;  }  if (stringRegEx.test(val)) {    return exports.TYPES.STRING;  }  if (angleRegEx.test(val)) {    return exports.TYPES.ANGLE;  }  if (colorRegEx1.test(val)) {    return exports.TYPES.COLOR;  }  var res = colorRegEx2.exec(val);  var parts;  if (res !== null) {    parts = res[1].split(/\s*,\s*/);    if (parts.length !== 3) {      return undefined;    }    if (      parts.every(percentRegEx.test.bind(percentRegEx)) ||      parts.every(integerRegEx.test.bind(integerRegEx))    ) {      return exports.TYPES.COLOR;    }    return undefined;  }  res = colorRegEx3.exec(val);  if (res !== null) {    parts = res[1].split(/\s*,\s*/);    if (parts.length !== 4) {      return undefined;    }    if (      parts.slice(0, 3).every(percentRegEx.test.bind(percentRegEx)) ||      parts.slice(0, 3).every(integerRegEx.test.bind(integerRegEx))    ) {      if (numberRegEx.test(parts[3])) {        return exports.TYPES.COLOR;      }    }    return undefined;  }  if (colorRegEx4.test(val)) {    return exports.TYPES.COLOR;  }  // could still be a color, one of the standard keyword colors  val = val.toLowerCase();  if (namedColors.includes(val)) {    return exports.TYPES.COLOR;  }  switch (val) {    // the following are deprecated in CSS3    case 'activeborder':    case 'activecaption':    case 'appworkspace':    case 'background':    case 'buttonface':    case 'buttonhighlight':    case 'buttonshadow':    case 'buttontext':    case 'captiontext':    case 'graytext':    case 'highlight':    case 'highlighttext':    case 'inactiveborder':    case 'inactivecaption':    case 'inactivecaptiontext':    case 'infobackground':    case 'infotext':    case 'menu':    case 'menutext':    case 'scrollbar':    case 'threeddarkshadow':    case 'threedface':    case 'threedhighlight':    case 'threedlightshadow':    case 'threedshadow':    case 'window':    case 'windowframe':    case 'windowtext':      return exports.TYPES.COLOR;    default:      return exports.TYPES.KEYWORD;  }};exports.parseInteger = function parseInteger(val) {  var type = exports.valueType(val);  if (type === exports.TYPES.NULL_OR_EMPTY_STR) {    return val;  }  if (type !== exports.TYPES.INTEGER) {    return undefined;  }  return String(parseInt(val, 10));};exports.parseNumber = function parseNumber(val) {  var type = exports.valueType(val);  if (type === exports.TYPES.NULL_OR_EMPTY_STR) {    return val;  }  if (type !== exports.TYPES.NUMBER && type !== exports.TYPES.INTEGER) {    return undefined;  }  return String(parseFloat(val));};exports.parseLength = function parseLength(val) {  if (val === 0 || val === '0') {    return '0px';  }  var type = exports.valueType(val);  if (type === exports.TYPES.NULL_OR_EMPTY_STR) {    return val;  }  if (type !== exports.TYPES.LENGTH) {    return undefined;  }  return val;};exports.parsePercent = function parsePercent(val) {  if (val === 0 || val === '0') {    return '0%';  }  var type = exports.valueType(val);  if (type === exports.TYPES.NULL_OR_EMPTY_STR) {    return val;  }  if (type !== exports.TYPES.PERCENT) {    return undefined;  }  return val;};// either a length or a percentexports.parseMeasurement = function parseMeasurement(val) {  var type = exports.valueType(val);  if (type === exports.TYPES.CALC) {    return val;  }  var length = exports.parseLength(val);  if (length !== undefined) {    return length;  }  return exports.parsePercent(val);};exports.parseUrl = function parseUrl(val) {  var type = exports.valueType(val);  if (type === exports.TYPES.NULL_OR_EMPTY_STR) {    return val;  }  var res = urlRegEx.exec(val);  // does it match the regex?  if (!res) {    return undefined;  }  var str = res[1];  // if it starts with single or double quotes, does it end with the same?  if ((str[0] === '"' || str[0] === "'") && str[0] !== str[str.length - 1]) {    return undefined;  }  if (str[0] === '"' || str[0] === "'") {    str = str.substr(1, str.length - 2);  }  var i;  for (i = 0; i < str.length; i++) {    switch (str[i]) {      case '(':      case ')':      case ' ':      case '\t':      case '\n':      case "'":      case '"':        return undefined;      case '\\':        i++;        break;    }  }  return 'url(' + str + ')';};exports.parseString = function parseString(val) {  var type = exports.valueType(val);  if (type === exports.TYPES.NULL_OR_EMPTY_STR) {    return val;  }  if (type !== exports.TYPES.STRING) {    return undefined;  }  var i;  for (i = 1; i < val.length - 1; i++) {    switch (val[i]) {      case val[0]:        return undefined;      case '\\':        i++;        while (i < val.length - 1 && /[0-9A-Fa-f]/.test(val[i])) {          i++;        }        break;    }  }  if (i >= val.length) {    return undefined;  }  return val;};exports.parseColor = function parseColor(val) {  var type = exports.valueType(val);  if (type === exports.TYPES.NULL_OR_EMPTY_STR) {    return val;  }  var red,    green,    blue,    hue,    saturation,    lightness,    alpha = 1;  var parts;  var res = colorRegEx1.exec(val);  // is it #aaa, #ababab, #aaaa, #abababaa  if (res) {    var defaultHex = val.substr(1);    var hex = val.substr(1);    if (hex.length === 3 || hex.length === 4) {      hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];      if (defaultHex.length === 4) {        hex = hex + defaultHex[3] + defaultHex[3];      }    }    red = parseInt(hex.substr(0, 2), 16);    green = parseInt(hex.substr(2, 2), 16);    blue = parseInt(hex.substr(4, 2), 16);    if (hex.length === 8) {      var hexAlpha = hex.substr(6, 2);      var hexAlphaToRgbaAlpha = Number((parseInt(hexAlpha, 16) / 255).toFixed(3));      return 'rgba(' + red + ', ' + green + ', ' + blue + ', ' + hexAlphaToRgbaAlpha + ')';    }    return 'rgb(' + red + ', ' + green + ', ' + blue + ')';  }  res = colorRegEx2.exec(val);  if (res) {    parts = res[1].split(/\s*,\s*/);    if (parts.length !== 3) {      return undefined;    }    if (parts.every(percentRegEx.test.bind(percentRegEx))) {      red = Math.floor((parseFloat(parts[0].slice(0, -1)) * 255) / 100);      green = Math.floor((parseFloat(parts[1].slice(0, -1)) * 255) / 100);      blue = Math.floor((parseFloat(parts[2].slice(0, -1)) * 255) / 100);    } else if (parts.every(integerRegEx.test.bind(integerRegEx))) {      red = parseInt(parts[0], 10);      green = parseInt(parts[1], 10);      blue = parseInt(parts[2], 10);    } else {      return undefined;    }    red = Math.min(255, Math.max(0, red));    green = Math.min(255, Math.max(0, green));    blue = Math.min(255, Math.max(0, blue));    return 'rgb(' + red + ', ' + green + ', ' + blue + ')';  }  res = colorRegEx3.exec(val);  if (res) {    parts = res[1].split(/\s*,\s*/);    if (parts.length !== 4) {      return undefined;    }    if (parts.slice(0, 3).every(percentRegEx.test.bind(percentRegEx))) {      red = Math.floor((parseFloat(parts[0].slice(0, -1)) * 255) / 100);      green = Math.floor((parseFloat(parts[1].slice(0, -1)) * 255) / 100);      blue = Math.floor((parseFloat(parts[2].slice(0, -1)) * 255) / 100);      alpha = parseFloat(parts[3]);    } else if (parts.slice(0, 3).every(integerRegEx.test.bind(integerRegEx))) {      red = parseInt(parts[0], 10);      green = parseInt(parts[1], 10);      blue = parseInt(parts[2], 10);      alpha = parseFloat(parts[3]);    } else {      return undefined;    }    if (isNaN(alpha)) {      alpha = 1;    }    red = Math.min(255, Math.max(0, red));    green = Math.min(255, Math.max(0, green));    blue = Math.min(255, Math.max(0, blue));    alpha = Math.min(1, Math.max(0, alpha));    if (alpha === 1) {      return 'rgb(' + red + ', ' + green + ', ' + blue + ')';    }    return 'rgba(' + red + ', ' + green + ', ' + blue + ', ' + alpha + ')';  }  res = colorRegEx4.exec(val);  if (res) {    const [, _hue, _saturation, _lightness, _alphaString = ''] = res;    const _alpha = parseFloat(_alphaString.replace(',', '').trim());    if (!_hue || !_saturation || !_lightness) {      return undefined;    }    hue = parseFloat(_hue);    saturation = parseInt(_saturation, 10);    lightness = parseInt(_lightness, 10);    if (_alpha && numberRegEx.test(_alpha)) {      alpha = parseFloat(_alpha);    }    const [r, g, b] = hslToRgb(hue, saturation / 100, lightness / 100);    if (!_alphaString || alpha === 1) {      return 'rgb(' + r + ', ' + g + ', ' + b + ')';    }    return 'rgba(' + r + ', ' + g + ', ' + b + ', ' + alpha + ')';  }  if (type === exports.TYPES.COLOR) {    return val;  }  return undefined;};exports.parseAngle = function parseAngle(val) {  var type = exports.valueType(val);  if (type === exports.TYPES.NULL_OR_EMPTY_STR) {    return val;  }  if (type !== exports.TYPES.ANGLE) {    return undefined;  }  var res = angleRegEx.exec(val);  var flt = parseFloat(res[1]);  if (res[2] === 'rad') {    flt *= 180 / Math.PI;  } else if (res[2] === 'grad') {    flt *= 360 / 400;  }  while (flt < 0) {    flt += 360;  }  while (flt > 360) {    flt -= 360;  }  return flt + 'deg';};exports.parseKeyword = function parseKeyword(val, valid_keywords) {  var type = exports.valueType(val);  if (type === exports.TYPES.NULL_OR_EMPTY_STR) {    return val;  }  if (type !== exports.TYPES.KEYWORD) {    return undefined;  }  val = val.toString().toLowerCase();  var i;  for (i = 0; i < valid_keywords.length; i++) {    if (valid_keywords[i].toLowerCase() === val) {      return valid_keywords[i];    }  }  return undefined;};// utility to translate from border-width to borderWidthvar dashedToCamelCase = function(dashed) {  var i;  var camel = '';  var nextCap = false;  for (i = 0; i < dashed.length; i++) {    if (dashed[i] !== '-') {      camel += nextCap ? dashed[i].toUpperCase() : dashed[i];      nextCap = false;    } else {      nextCap = true;    }  }  return camel;};exports.dashedToCamelCase = dashedToCamelCase;var is_space = /\s/;var opening_deliminators = ['"', "'", '('];var closing_deliminators = ['"', "'", ')'];// this splits on whitespace, but keeps quoted and parened parts togethervar getParts = function(str) {  var deliminator_stack = [];  var length = str.length;  var i;  var parts = [];  var current_part = '';  var opening_index;  var closing_index;  for (i = 0; i < length; i++) {    opening_index = opening_deliminators.indexOf(str[i]);    closing_index = closing_deliminators.indexOf(str[i]);    if (is_space.test(str[i])) {      if (deliminator_stack.length === 0) {        if (current_part !== '') {          parts.push(current_part);        }        current_part = '';      } else {        current_part += str[i];      }    } else {      if (str[i] === '\\') {        i++;        current_part += str[i];      } else {        current_part += str[i];        if (          closing_index !== -1 &&          closing_index === deliminator_stack[deliminator_stack.length - 1]        ) {          deliminator_stack.pop();        } else if (opening_index !== -1) {          deliminator_stack.push(opening_index);        }      }    }  }  if (current_part !== '') {    parts.push(current_part);  }  return parts;};/* * this either returns undefined meaning that it isn't valid * or returns an object where the keys are dashed short * hand properties and the values are the values to set * on them */exports.shorthandParser = function parse(v, shorthand_for) {  var obj = {};  var type = exports.valueType(v);  if (type === exports.TYPES.NULL_OR_EMPTY_STR) {    Object.keys(shorthand_for).forEach(function(property) {      obj[property] = '';    });    return obj;  }  if (typeof v === 'number') {    v = v.toString();  }  if (typeof v !== 'string') {    return undefined;  }  if (v.toLowerCase() === 'inherit') {    return {};  }  var parts = getParts(v);  var valid = true;  parts.forEach(function(part, i) {    var part_valid = false;    Object.keys(shorthand_for).forEach(function(property) {      if (shorthand_for[property].isValid(part, i)) {        part_valid = true;        obj[property] = part;      }    });    valid = valid && part_valid;  });  if (!valid) {    return undefined;  }  return obj;};exports.shorthandSetter = function(property, shorthand_for) {  return function(v) {    var obj = exports.shorthandParser(v, shorthand_for);    if (obj === undefined) {      return;    }    //console.log('shorthandSetter for:', property, 'obj:', obj);    Object.keys(obj).forEach(function(subprop) {      // in case subprop is an implicit property, this will clear      // *its* subpropertiesX      var camel = dashedToCamelCase(subprop);      this[camel] = obj[subprop];      // in case it gets translated into something else (0 -> 0px)      obj[subprop] = this[camel];      this.removeProperty(subprop);      // don't add in empty properties      if (obj[subprop] !== '') {        this._values[subprop] = obj[subprop];      }    }, this);    Object.keys(shorthand_for).forEach(function(subprop) {      if (!obj.hasOwnProperty(subprop)) {        this.removeProperty(subprop);        delete this._values[subprop];      }    }, this);    // in case the value is something like 'none' that removes all values,    // check that the generated one is not empty, first remove the property    // if it already exists, then call the shorthandGetter, if it's an empty    // string, don't set the property    this.removeProperty(property);    var calculated = exports.shorthandGetter(property, shorthand_for).call(this);    if (calculated !== '') {      this._setProperty(property, calculated);    }  };};exports.shorthandGetter = function(property, shorthand_for) {  return function() {    if (this._values[property] !== undefined) {      return this.getPropertyValue(property);    }    return Object.keys(shorthand_for)      .map(function(subprop) {        return this.getPropertyValue(subprop);      }, this)      .filter(function(value) {        return value !== '';      })      .join(' ');  };};// isValid(){1,4} | inherit// if one, it applies to all// if two, the first applies to the top and bottom, and the second to left and right// if three, the first applies to the top, the second to left and right, the third bottom// if four, top, right, bottom, leftexports.implicitSetter = function(property_before, property_after, isValid, parser) {  property_after = property_after || '';  if (property_after !== '') {    property_after = '-' + property_after;  }  var part_names = ['top', 'right', 'bottom', 'left'];  return function(v) {    if (typeof v === 'number') {      v = v.toString();    }    if (typeof v !== 'string') {      return undefined;    }    var parts;    if (v.toLowerCase() === 'inherit' || v === '') {      parts = [v];    } else {      parts = getParts(v);    }    if (parts.length < 1 || parts.length > 4) {      return undefined;    }    if (!parts.every(isValid)) {      return undefined;    }    parts = parts.map(function(part) {      return parser(part);    });    this._setProperty(property_before + property_after, parts.join(' '));    if (parts.length === 1) {      parts[1] = parts[0];    }    if (parts.length === 2) {      parts[2] = parts[0];    }    if (parts.length === 3) {      parts[3] = parts[1];    }    for (var i = 0; i < 4; i++) {      var property = property_before + '-' + part_names[i] + property_after;      this.removeProperty(property);      if (parts[i] !== '') {        this._values[property] = parts[i];      }    }    return v;  };};////  Companion to implicitSetter, but for the individual parts.//  This sets the individual value, and checks to see if all four//  sub-parts are set.  If so, it sets the shorthand version and removes//  the individual parts from the cssText.//exports.subImplicitSetter = function(prefix, part, isValid, parser) {  var property = prefix + '-' + part;  var subparts = [prefix + '-top', prefix + '-right', prefix + '-bottom', prefix + '-left'];  return function(v) {    if (typeof v === 'number') {      v = v.toString();    }    if (typeof v !== 'string') {      return undefined;    }    if (!isValid(v)) {      return undefined;    }    v = parser(v);    this._setProperty(property, v);    var parts = [];    for (var i = 0; i < 4; i++) {      if (this._values[subparts[i]] == null || this._values[subparts[i]] === '') {        break;      }      parts.push(this._values[subparts[i]]);    }    if (parts.length === 4) {      for (i = 0; i < 4; i++) {        this.removeProperty(subparts[i]);        this._values[subparts[i]] = parts[i];      }      this._setProperty(prefix, parts.join(' '));    }    return v;  };};var camel_to_dashed = /[A-Z]/g;var first_segment = /^\([^-]\)-/;var vendor_prefixes = ['o', 'moz', 'ms', 'webkit'];exports.camelToDashed = function(camel_case) {  var match;  var dashed = camel_case.replace(camel_to_dashed, '-$&').toLowerCase();  match = dashed.match(first_segment);  if (match && vendor_prefixes.indexOf(match[1]) !== -1) {    dashed = '-' + dashed;  }  return dashed;};
 |