Merge branch 'master' of github.com:airbnb/lottie-web
diff --git a/player/js/effects/TransformEffect.js b/player/js/effects/TransformEffect.js
index d4bcc0d..2536ce5 100644
--- a/player/js/effects/TransformEffect.js
+++ b/player/js/effects/TransformEffect.js
@@ -21,8 +21,9 @@
     var effectElements = this.effectsManager.effectElements;
     var anchor = effectElements[0].p.v;
     var position = effectElements[1].p.v;
+    var isUniformScale = effectElements[2].p.v === 1;
     var scaleHeight = effectElements[3].p.v;
-    var scaleWidth = effectElements[4].p.v;
+    var scaleWidth = isUniformScale ? scaleHeight : effectElements[4].p.v;
     var skew = effectElements[5].p.v;
     var skewAxis = effectElements[6].p.v;
     var rotation = effectElements[7].p.v;
diff --git a/player/js/elements/helpers/TransformElement.js b/player/js/elements/helpers/TransformElement.js
index cbb2391..bc27023 100644
--- a/player/js/elements/helpers/TransformElement.js
+++ b/player/js/elements/helpers/TransformElement.js
@@ -70,7 +70,8 @@
           if (this.localTransforms[i]._mdf) {
             this.finalTransform._localMatMdf = true;
           }
-          if (this.localTransforms[i]._opMdf) {
+          if (this.localTransforms[i]._opMdf && !this.finalTransform._opMdf) {
+            this.finalTransform.localOpacity = this.finalTransform.mProp.o.v;
             this.finalTransform._opMdf = true;
           }
           i += 1;
diff --git a/player/js/utils/FontManager.js b/player/js/utils/FontManager.js
index 4f749fb..6477bcc 100644
--- a/player/js/utils/FontManager.js
+++ b/player/js/utils/FontManager.js
@@ -18,6 +18,15 @@
     2367, 2368, 2369, 2370, 2371, 2372, 2373, 2374, 2375, 2376, 2377, 2378, 2379,
     2380, 2381, 2382, 2383, 2387, 2388, 2389, 2390, 2391, 2402, 2403]);
 
+  var BLACK_FLAG_CODE_POINT = 127988;
+  var CANCEL_TAG_CODE_POINT = 917631;
+  var A_TAG_CODE_POINT = 917601;
+  var Z_TAG_CODE_POINT = 917626;
+  var VARIATION_SELECTOR_16_CODE_POINT = 65039;
+  var ZERO_WIDTH_JOINER_CODE_POINT = 8205;
+  var REGIONAL_CHARACTER_A_CODE_POINT = 127462;
+  var REGIONAL_CHARACTER_Z_CODE_POINT = 127487;
+
   var surrogateModifiers = [
     'd83cdffb',
     'd83cdffc',
@@ -26,8 +35,6 @@
     'd83cdfff',
   ];
 
-  var zeroWidthJoiner = [65039, 8205];
-
   function trimFontOptions(font) {
     var familyArray = font.split(',');
     var i;
@@ -296,18 +303,20 @@
 
   function measureText(char, fontName, size) {
     var fontData = this.getFontByName(fontName);
-    var index = char.charCodeAt(0);
-    if (!fontData.cache[index + 1]) {
+    // Using the char instead of char.charCodeAt(0)
+    // to avoid collisions between equal chars
+    var index = char;
+    if (!fontData.cache[index]) {
       var tHelper = fontData.helper;
       if (char === ' ') {
         var doubleSize = tHelper.measureText('|' + char + '|');
         var singleSize = tHelper.measureText('||');
-        fontData.cache[index + 1] = (doubleSize - singleSize) / 100;
+        fontData.cache[index] = (doubleSize - singleSize) / 100;
       } else {
-        fontData.cache[index + 1] = tHelper.measureText(char) / 100;
+        fontData.cache[index] = tHelper.measureText(char) / 100;
       }
     }
-    return fontData.cache[index + 1] * size;
+    return fontData.cache[index] * size;
   }
 
   function getFontByName(name) {
@@ -322,22 +331,77 @@
     return this.fonts[0];
   }
 
+  function getCodePoint(string) {
+    var codePoint = 0;
+    var first = string.charCodeAt(0);
+    if (first >= 0xD800 && first <= 0xDBFF) {
+      var second = string.charCodeAt(1);
+      if (second >= 0xDC00 && second <= 0xDFFF) {
+        codePoint = (first - 0xD800) * 0x400 + second - 0xDC00 + 0x10000;
+      }
+    }
+    return codePoint;
+  }
+
+  // Skin tone modifiers
   function isModifier(firstCharCode, secondCharCode) {
     var sum = firstCharCode.toString(16) + secondCharCode.toString(16);
     return surrogateModifiers.indexOf(sum) !== -1;
   }
 
-  function isZeroWidthJoiner(firstCharCode, secondCharCode) {
-    if (!secondCharCode) {
-      return firstCharCode === zeroWidthJoiner[1];
+  function isZeroWidthJoiner(charCode) {
+    return charCode === ZERO_WIDTH_JOINER_CODE_POINT;
+  }
+
+  // This codepoint may change the appearance of the preceding character.
+  // If that is a symbol, dingbat or emoji, U+FE0F forces it to be rendered
+  // as a colorful image as compared to a monochrome text variant.
+  function isVariationSelector(charCode) {
+    return charCode === VARIATION_SELECTOR_16_CODE_POINT;
+  }
+
+  // The regional indicator symbols are a set of 26 alphabetic Unicode
+  /// characters (A–Z) intended to be used to encode ISO 3166-1 alpha-2
+  // two-letter country codes in a way that allows optional special treatment.
+  function isRegionalCode(string) {
+    var codePoint = getCodePoint(string);
+    if (codePoint >= REGIONAL_CHARACTER_A_CODE_POINT && codePoint <= REGIONAL_CHARACTER_Z_CODE_POINT) {
+      return true;
     }
-    return firstCharCode === zeroWidthJoiner[0] && secondCharCode === zeroWidthJoiner[1];
+    return false;
+  }
+
+  // Some Emoji implementations represent combinations of
+  // two “regional indicator” letters as a single flag symbol.
+  function isFlagEmoji(string) {
+    return isRegionalCode(string.substr(0, 2)) && isRegionalCode(string.substr(2, 2));
   }
 
   function isCombinedCharacter(char) {
     return combinedCharacters.indexOf(char) !== -1;
   }
 
+  // Regional flags start with a BLACK_FLAG_CODE_POINT
+  // folowed by 5 chars in the TAG range
+  // and end with a CANCEL_TAG_CODE_POINT
+  function isRegionalFlag(text, index) {
+    var codePoint = getCodePoint(text.substr(index, 2));
+    if (codePoint !== BLACK_FLAG_CODE_POINT) {
+      return false;
+    }
+    var count = 0;
+    index += 2;
+    while (count < 5) {
+      codePoint = getCodePoint(text.substr(index, 2));
+      if (codePoint < A_TAG_CODE_POINT || codePoint > Z_TAG_CODE_POINT) {
+        return false;
+      }
+      count += 1;
+      index += 2;
+    }
+    return getCodePoint(text.substr(index, 2)) === CANCEL_TAG_CODE_POINT;
+  }
+
   function setIsLoaded() {
     this.isLoaded = true;
   }
@@ -354,7 +418,12 @@
   };
   Font.isModifier = isModifier;
   Font.isZeroWidthJoiner = isZeroWidthJoiner;
+  Font.isFlagEmoji = isFlagEmoji;
+  Font.isRegionalCode = isRegionalCode;
   Font.isCombinedCharacter = isCombinedCharacter;
+  Font.isRegionalFlag = isRegionalFlag;
+  Font.isVariationSelector = isVariationSelector;
+  Font.BLACK_FLAG_CODE_POINT = BLACK_FLAG_CODE_POINT;
 
   var fontPrototype = {
     addChars: addChars,
diff --git a/player/js/utils/expressions/ExpressionManager.js b/player/js/utils/expressions/ExpressionManager.js
index 20acb03..121763e 100644
--- a/player/js/utils/expressions/ExpressionManager.js
+++ b/player/js/utils/expressions/ExpressionManager.js
@@ -34,12 +34,12 @@
   }
 
   function isNumerable(tOfV, v) {
-    return tOfV === 'number' || tOfV === 'boolean' || tOfV === 'string' || v instanceof Number;
+    return tOfV === 'number' || v instanceof Number || tOfV === 'boolean' || tOfV === 'string';
   }
 
   function $bm_neg(a) {
     var tOfA = typeof a;
-    if (tOfA === 'number' || tOfA === 'boolean' || a instanceof Number) {
+    if (tOfA === 'number' || a instanceof Number || tOfA === 'boolean') {
       return -a;
     }
     if ($bm_isInstanceOfArray(a)) {
@@ -64,10 +64,7 @@
   function sum(a, b) {
     var tOfA = typeof a;
     var tOfB = typeof b;
-    if (tOfA === 'string' || tOfB === 'string') {
-      return a + b;
-    }
-    if (isNumerable(tOfA, a) && isNumerable(tOfB, b)) {
+    if ((isNumerable(tOfA, a) && isNumerable(tOfB, b)) || tOfA === 'string' || tOfB === 'string') {
       return a + b;
     }
     if ($bm_isInstanceOfArray(a) && isNumerable(tOfB, b)) {
diff --git a/player/js/utils/text/TextProperty.js b/player/js/utils/text/TextProperty.js
index 1b188f0..a26ddf6 100644
--- a/player/js/utils/text/TextProperty.js
+++ b/player/js/utils/text/TextProperty.js
@@ -153,39 +153,49 @@
   var charCode;
   var secondCharCode;
   var shouldCombine = false;
+  var shouldCombineNext = false;
+  var currentChars = '';
   while (i < len) {
+    shouldCombine = shouldCombineNext;
+    shouldCombineNext = false;
     charCode = text.charCodeAt(i);
+    currentChars = text.charAt(i);
     if (FontManager.isCombinedCharacter(charCode)) {
-      charactersArray[charactersArray.length - 1] += text.charAt(i);
+      shouldCombine = true;
+      // It's a potential surrogate pair (this is the High surrogate)
     } else if (charCode >= 0xD800 && charCode <= 0xDBFF) {
-      secondCharCode = text.charCodeAt(i + 1);
-      if (secondCharCode >= 0xDC00 && secondCharCode <= 0xDFFF) {
-        if (shouldCombine || FontManager.isModifier(charCode, secondCharCode)) {
-          charactersArray[charactersArray.length - 1] += text.substr(i, 2);
-          shouldCombine = false;
-        } else {
-          charactersArray.push(text.substr(i, 2));
-        }
-        i += 1;
+      if (FontManager.isRegionalFlag(text, i)) {
+        currentChars = text.substr(i, 14);
       } else {
-        charactersArray.push(text.charAt(i));
+        secondCharCode = text.charCodeAt(i + 1);
+        // It's a surrogate pair (this is the Low surrogate)
+        if (secondCharCode >= 0xDC00 && secondCharCode <= 0xDFFF) {
+          if (FontManager.isModifier(charCode, secondCharCode)) {
+            currentChars = text.substr(i, 2);
+            shouldCombine = true;
+          } else if (FontManager.isFlagEmoji(text.substr(i, 4))) {
+            currentChars = text.substr(i, 4);
+          } else {
+            currentChars = text.substr(i, 2);
+          }
+        }
       }
     } else if (charCode > 0xDBFF) {
       secondCharCode = text.charCodeAt(i + 1);
-      if (FontManager.isZeroWidthJoiner(charCode, secondCharCode)) {
+      if (FontManager.isVariationSelector(charCode)) {
         shouldCombine = true;
-        charactersArray[charactersArray.length - 1] += text.substr(i, 2);
-        i += 1;
-      } else {
-        charactersArray.push(text.charAt(i));
       }
     } else if (FontManager.isZeroWidthJoiner(charCode)) {
-      charactersArray[charactersArray.length - 1] += text.charAt(i);
       shouldCombine = true;
-    } else {
-      charactersArray.push(text.charAt(i));
+      shouldCombineNext = true;
     }
-    i += 1;
+    if (shouldCombine) {
+      charactersArray[charactersArray.length - 1] += currentChars;
+      shouldCombine = false;
+    } else {
+      charactersArray.push(currentChars);
+    }
+    i += currentChars.length;
   }
   return charactersArray;
 };