1/** 2@license 3Copyright (c) 2017 The Polymer Project Authors. All rights reserved. 4This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt 5The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt 6The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt 7Code distributed by Google as part of the polymer project is also 8subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt 9*/ 10 11'use strict'; 12 13import {removeCustomPropAssignment, StyleNode} from './css-parse.js'; // eslint-disable-line no-unused-vars 14import {nativeShadow} from './style-settings.js'; 15import StyleTransformer from './style-transformer.js'; 16import * as StyleUtil from './style-util.js'; 17import * as RX from './common-regex.js'; 18import StyleInfo from './style-info.js'; 19 20// TODO: dedupe with shady 21/** 22 * @param {string} selector 23 * @return {boolean} 24 * @this {Element} 25 */ 26const matchesSelector = function(selector) { 27 const method = this.matches || this.matchesSelector || 28 this.mozMatchesSelector || this.msMatchesSelector || 29 this.oMatchesSelector || this.webkitMatchesSelector; 30 return method && method.call(this, selector); 31}; 32 33const IS_IE = navigator.userAgent.match('Trident'); 34 35const XSCOPE_NAME = 'x-scope'; 36 37class StyleProperties { 38 get XSCOPE_NAME() { 39 return XSCOPE_NAME; 40 } 41/** 42 * decorates styles with rule info and returns an array of used style property names 43 * 44 * @param {StyleNode} rules 45 * @return {Array<string>} 46 */ 47 decorateStyles(rules) { 48 let self = this, props = {}, keyframes = [], ruleIndex = 0; 49 StyleUtil.forEachRule(rules, function(rule) { 50 self.decorateRule(rule); 51 // mark in-order position of ast rule in styles block, used for cache key 52 rule.index = ruleIndex++; 53 self.collectPropertiesInCssText(rule.propertyInfo.cssText, props); 54 }, function onKeyframesRule(rule) { 55 keyframes.push(rule); 56 }); 57 // Cache all found keyframes rules for later reference: 58 rules._keyframes = keyframes; 59 // return this list of property names *consumes* in these styles. 60 let names = []; 61 for (let i in props) { 62 names.push(i); 63 } 64 return names; 65 } 66 67 // decorate a single rule with property info 68 decorateRule(rule) { 69 if (rule.propertyInfo) { 70 return rule.propertyInfo; 71 } 72 let info = {}, properties = {}; 73 let hasProperties = this.collectProperties(rule, properties); 74 if (hasProperties) { 75 info.properties = properties; 76 // TODO(sorvell): workaround parser seeing mixins as additional rules 77 rule['rules'] = null; 78 } 79 info.cssText = this.collectCssText(rule); 80 rule.propertyInfo = info; 81 return info; 82 } 83 84 // collects the custom properties from a rule's cssText 85 collectProperties(rule, properties) { 86 let info = rule.propertyInfo; 87 if (info) { 88 if (info.properties) { 89 Object.assign(properties, info.properties); 90 return true; 91 } 92 } else { 93 let m, rx = RX.VAR_ASSIGN; 94 let cssText = rule['parsedCssText']; 95 let value; 96 let any; 97 while ((m = rx.exec(cssText))) { 98 // note: group 2 is var, 3 is mixin 99 value = (m[2] || m[3]).trim(); 100 // value of 'inherit' or 'unset' is equivalent to not setting the property here 101 if (value !== 'inherit' || value !== 'unset') { 102 properties[m[1].trim()] = value; 103 } 104 any = true; 105 } 106 return any; 107 } 108 109 } 110 111 // returns cssText of properties that consume variables/mixins 112 collectCssText(rule) { 113 return this.collectConsumingCssText(rule['parsedCssText']); 114 } 115 116 // NOTE: we support consumption inside mixin assignment 117 // but not production, so strip out {...} 118 collectConsumingCssText(cssText) { 119 return cssText.replace(RX.BRACKETED, '') 120 .replace(RX.VAR_ASSIGN, ''); 121 } 122 123 collectPropertiesInCssText(cssText, props) { 124 let m; 125 while ((m = RX.VAR_CONSUMED.exec(cssText))) { 126 let name = m[1]; 127 // This regex catches all variable names, and following non-whitespace char 128 // If next char is not ':', then variable is a consumer 129 if (m[2] !== ':') { 130 props[name] = true; 131 } 132 } 133 } 134 135 // turns custom properties into realized values. 136 reify(props) { 137 // big perf optimization here: reify only *own* properties 138 // since this object has __proto__ of the element's scope properties 139 let names = Object.getOwnPropertyNames(props); 140 for (let i=0, n; i < names.length; i++) { 141 n = names[i]; 142 props[n] = this.valueForProperty(props[n], props); 143 } 144 } 145 146 // given a property value, returns the reified value 147 // a property value may be: 148 // (1) a literal value like: red or 5px; 149 // (2) a variable value like: var(--a), var(--a, red), or var(--a, --b) or 150 // var(--a, var(--b)); 151 // (3) a literal mixin value like { properties }. Each of these properties 152 // can have values that are: (a) literal, (b) variables, (c) @apply mixins. 153 valueForProperty(property, props) { 154 // case (1) default 155 // case (3) defines a mixin and we have to reify the internals 156 if (property) { 157 if (property.indexOf(';') >=0) { 158 property = this.valueForProperties(property, props); 159 } else { 160 // case (2) variable 161 let self = this; 162 let fn = function(prefix, value, fallback, suffix) { 163 if (!value) { 164 return prefix + suffix; 165 } 166 let propertyValue = self.valueForProperty(props[value], props); 167 // if value is "initial", then the variable should be treated as unset 168 if (!propertyValue || propertyValue === 'initial') { 169 // fallback may be --a or var(--a) or literal 170 propertyValue = self.valueForProperty(props[fallback] || fallback, props) || 171 fallback; 172 } else if (propertyValue === 'apply-shim-inherit') { 173 // CSS build will replace `inherit` with `apply-shim-inherit` 174 // for use with native css variables. 175 // Since we have full control, we can use `inherit` directly. 176 propertyValue = 'inherit'; 177 } 178 return prefix + (propertyValue || '') + suffix; 179 }; 180 property = StyleUtil.processVariableAndFallback(property, fn); 181 } 182 } 183 return property && property.trim() || ''; 184 } 185 186 // note: we do not yet support mixin within mixin 187 valueForProperties(property, props) { 188 let parts = property.split(';'); 189 for (let i=0, p, m; i<parts.length; i++) { 190 if ((p = parts[i])) { 191 RX.MIXIN_MATCH.lastIndex = 0; 192 m = RX.MIXIN_MATCH.exec(p); 193 if (m) { 194 p = this.valueForProperty(props[m[1]], props); 195 } else { 196 let colon = p.indexOf(':'); 197 if (colon !== -1) { 198 let pp = p.substring(colon); 199 pp = pp.trim(); 200 pp = this.valueForProperty(pp, props) || pp; 201 p = p.substring(0, colon) + pp; 202 } 203 } 204 parts[i] = (p && p.lastIndexOf(';') === p.length - 1) ? 205 // strip trailing ; 206 p.slice(0, -1) : 207 p || ''; 208 } 209 } 210 return parts.join(';'); 211 } 212 213 applyProperties(rule, props) { 214 let output = ''; 215 // dynamically added sheets may not be decorated so ensure they are. 216 if (!rule.propertyInfo) { 217 this.decorateRule(rule); 218 } 219 if (rule.propertyInfo.cssText) { 220 output = this.valueForProperties(rule.propertyInfo.cssText, props); 221 } 222 rule['cssText'] = output; 223 } 224 225 // Apply keyframe transformations to the cssText of a given rule. The 226 // keyframeTransforms object is a map of keyframe names to transformer 227 // functions which take in cssText and spit out transformed cssText. 228 applyKeyframeTransforms(rule, keyframeTransforms) { 229 let input = rule['cssText']; 230 let output = rule['cssText']; 231 if (rule.hasAnimations == null) { 232 // Cache whether or not the rule has any animations to begin with: 233 rule.hasAnimations = RX.ANIMATION_MATCH.test(input); 234 } 235 // If there are no animations referenced, we can skip transforms: 236 if (rule.hasAnimations) { 237 let transform; 238 // If we haven't transformed this rule before, we iterate over all 239 // transforms: 240 if (rule.keyframeNamesToTransform == null) { 241 rule.keyframeNamesToTransform = []; 242 for (let keyframe in keyframeTransforms) { 243 transform = keyframeTransforms[keyframe]; 244 output = transform(input); 245 // If the transform actually changed the CSS text, we cache the 246 // transform name for future use: 247 if (input !== output) { 248 input = output; 249 rule.keyframeNamesToTransform.push(keyframe); 250 } 251 } 252 } else { 253 // If we already have a list of keyframe names that apply to this 254 // rule, we apply only those keyframe name transforms: 255 for (let i = 0; i < rule.keyframeNamesToTransform.length; ++i) { 256 transform = keyframeTransforms[rule.keyframeNamesToTransform[i]]; 257 input = transform(input); 258 } 259 output = input; 260 } 261 } 262 rule['cssText'] = output; 263 } 264 265 // Test if the rules in these styles matches the given `element` and if so, 266 // collect any custom properties into `props`. 267 /** 268 * @param {StyleNode} rules 269 * @param {Element} element 270 */ 271 propertyDataFromStyles(rules, element) { 272 let props = {}; 273 // generates a unique key for these matches 274 let o = []; 275 // note: active rules excludes non-matching @media rules 276 StyleUtil.forEachRule(rules, (rule) => { 277 // TODO(sorvell): we could trim the set of rules at declaration 278 // time to only include ones that have properties 279 if (!rule.propertyInfo) { 280 this.decorateRule(rule); 281 } 282 // match element against transformedSelector: selector may contain 283 // unwanted uniquification and parsedSelector does not directly match 284 // for :host selectors. 285 let selectorToMatch = rule.transformedSelector || rule['parsedSelector']; 286 if (element && rule.propertyInfo.properties && selectorToMatch) { 287 if (matchesSelector.call(element, selectorToMatch)) { 288 this.collectProperties(rule, props); 289 // produce numeric key for these matches for lookup 290 addToBitMask(rule.index, o); 291 } 292 } 293 }, null, true); 294 return {properties: props, key: o}; 295 } 296 297 /** 298 * @param {Element} scope 299 * @param {StyleNode} rule 300 * @param {string} cssBuild 301 * @param {function(Object)} callback 302 */ 303 whenHostOrRootRule(scope, rule, cssBuild, callback) { 304 if (!rule.propertyInfo) { 305 this.decorateRule(rule); 306 } 307 if (!rule.propertyInfo.properties) { 308 return; 309 } 310 let {is, typeExtension} = StyleUtil.getIsExtends(scope); 311 let hostScope = is ? 312 StyleTransformer._calcHostScope(is, typeExtension) : 313 'html'; 314 let parsedSelector = rule['parsedSelector']; 315 let isRoot = (parsedSelector === ':host > *' || parsedSelector === 'html'); 316 let isHost = parsedSelector.indexOf(':host') === 0 && !isRoot; 317 // build info is either in scope (when scope is an element) or in the style 318 // when scope is the default scope; note: this allows default scope to have 319 // mixed mode built and unbuilt styles. 320 if (cssBuild === 'shady') { 321 // :root -> x-foo > *.x-foo for elements and html for custom-style 322 isRoot = parsedSelector === (hostScope + ' > *.' + hostScope) || parsedSelector.indexOf('html') !== -1; 323 // :host -> x-foo for elements, but sub-rules have .x-foo in them 324 isHost = !isRoot && parsedSelector.indexOf(hostScope) === 0; 325 } 326 if (!isRoot && !isHost) { 327 return; 328 } 329 let selectorToMatch = hostScope; 330 if (isHost) { 331 // need to transform :host because `:host` does not work with `matches` 332 if (!rule.transformedSelector) { 333 // transform :host into a matchable selector 334 rule.transformedSelector = 335 StyleTransformer._transformRuleCss( 336 rule, 337 StyleTransformer._transformComplexSelector, 338 StyleTransformer._calcElementScope(is), 339 hostScope 340 ); 341 } 342 selectorToMatch = rule.transformedSelector || hostScope; 343 } 344 callback({ 345 selector: selectorToMatch, 346 isHost: isHost, 347 isRoot: isRoot 348 }); 349 } 350/** 351 * @param {Element} scope 352 * @param {StyleNode} rules 353 * @param {string} cssBuild 354 * @return {Object} 355 */ 356 hostAndRootPropertiesForScope(scope, rules, cssBuild) { 357 let hostProps = {}, rootProps = {}; 358 // note: active rules excludes non-matching @media rules 359 StyleUtil.forEachRule(rules, (rule) => { 360 // if scope is StyleDefaults, use _element for matchesSelector 361 this.whenHostOrRootRule(scope, rule, cssBuild, (info) => { 362 let element = scope._element || scope; 363 if (matchesSelector.call(element, info.selector)) { 364 if (info.isHost) { 365 this.collectProperties(rule, hostProps); 366 } else { 367 this.collectProperties(rule, rootProps); 368 } 369 } 370 }); 371 }, null, true); 372 return {rootProps: rootProps, hostProps: hostProps}; 373 } 374 375 /** 376 * @param {Element} element 377 * @param {Object} properties 378 * @param {string} scopeSelector 379 */ 380 transformStyles(element, properties, scopeSelector) { 381 let self = this; 382 let {is, typeExtension} = StyleUtil.getIsExtends(element); 383 let hostSelector = StyleTransformer 384 ._calcHostScope(is, typeExtension); 385 let rxHostSelector = element.extends ? 386 '\\' + hostSelector.slice(0, -1) + '\\]' : 387 hostSelector; 388 let hostRx = new RegExp(RX.HOST_PREFIX + rxHostSelector + 389 RX.HOST_SUFFIX); 390 let {styleRules: rules, cssBuild} = StyleInfo.get(element); 391 let keyframeTransforms = 392 this._elementKeyframeTransforms(element, rules, scopeSelector); 393 return StyleTransformer.elementStyles(element, rules, function(rule) { 394 self.applyProperties(rule, properties); 395 if (!nativeShadow && 396 !StyleUtil.isKeyframesSelector(rule) && 397 rule['cssText']) { 398 // NOTE: keyframe transforms only scope munge animation names, so it 399 // is not necessary to apply them in ShadowDOM. 400 self.applyKeyframeTransforms(rule, keyframeTransforms); 401 self._scopeSelector(rule, hostRx, hostSelector, scopeSelector); 402 } 403 }, cssBuild); 404 } 405 406 /** 407 * @param {Element} element 408 * @param {StyleNode} rules 409 * @param {string} scopeSelector 410 * @return {Object} 411 */ 412 _elementKeyframeTransforms(element, rules, scopeSelector) { 413 let keyframesRules = rules._keyframes; 414 let keyframeTransforms = {}; 415 if (!nativeShadow && keyframesRules) { 416 // For non-ShadowDOM, we transform all known keyframes rules in 417 // advance for the current scope. This allows us to catch keyframes 418 // rules that appear anywhere in the stylesheet: 419 for (let i = 0, keyframesRule = keyframesRules[i]; 420 i < keyframesRules.length; 421 keyframesRule = keyframesRules[++i]) { 422 this._scopeKeyframes(keyframesRule, scopeSelector); 423 keyframeTransforms[keyframesRule['keyframesName']] = 424 this._keyframesRuleTransformer(keyframesRule); 425 } 426 } 427 return keyframeTransforms; 428 } 429 430 // Generate a factory for transforming a chunk of CSS text to handle a 431 // particular scoped keyframes rule. 432 /** 433 * @param {StyleNode} keyframesRule 434 * @return {function(string):string} 435 */ 436 _keyframesRuleTransformer(keyframesRule) { 437 return function(cssText) { 438 return cssText.replace( 439 keyframesRule.keyframesNameRx, 440 keyframesRule.transformedKeyframesName); 441 }; 442 } 443 444/** 445 * Transforms `@keyframes` names to be unique for the current host. 446 * Example: @keyframes foo-anim -> @keyframes foo-anim-x-foo-0 447 * 448 * @param {StyleNode} rule 449 * @param {string} scopeId 450 */ 451 _scopeKeyframes(rule, scopeId) { 452 // Animation names are of the form [\w-], so ensure that the name regex does not partially apply 453 // to similarly named keyframe names by checking for a word boundary at the beginning and 454 // a non-word boundary or `-` at the end. 455 rule.keyframesNameRx = new RegExp(`\\b${rule['keyframesName']}(?!\\B|-)`, 'g'); 456 rule.transformedKeyframesName = rule['keyframesName'] + '-' + scopeId; 457 rule.transformedSelector = rule.transformedSelector || rule['selector']; 458 rule['selector'] = rule.transformedSelector.replace( 459 rule['keyframesName'], rule.transformedKeyframesName); 460 } 461 462 // Strategy: x scope shim a selector e.g. to scope `.x-foo-42` (via classes): 463 // non-host selector: .a.x-foo -> .x-foo-42 .a.x-foo 464 // host selector: x-foo.wide -> .x-foo-42.wide 465 // note: we use only the scope class (.x-foo-42) and not the hostSelector 466 // (x-foo) to scope :host rules; this helps make property host rules 467 // have low specificity. They are overrideable by class selectors but, 468 // unfortunately, not by type selectors (e.g. overriding via 469 // `.special` is ok, but not by `x-foo`). 470 /** 471 * @param {StyleNode} rule 472 * @param {RegExp} hostRx 473 * @param {string} hostSelector 474 * @param {string} scopeId 475 */ 476 _scopeSelector(rule, hostRx, hostSelector, scopeId) { 477 rule.transformedSelector = rule.transformedSelector || rule['selector']; 478 let selector = rule.transformedSelector; 479 let scope = '.' + scopeId; 480 let parts = StyleUtil.splitSelectorList(selector); 481 for (let i=0, l=parts.length, p; (i<l) && (p=parts[i]); i++) { 482 parts[i] = p.match(hostRx) ? 483 p.replace(hostSelector, scope) : 484 scope + ' ' + p; 485 } 486 rule['selector'] = parts.join(','); 487 } 488 489 /** 490 * @param {Element} element 491 * @param {string} selector 492 * @param {string} old 493 */ 494 applyElementScopeSelector(element, selector, old) { 495 let c = element.getAttribute('class') || ''; 496 let v = c; 497 if (old) { 498 v = c.replace( 499 new RegExp('\\s*' + XSCOPE_NAME + '\\s*' + old + '\\s*', 'g'), ' '); 500 } 501 v += (v ? ' ' : '') + XSCOPE_NAME + ' ' + selector; 502 if (c !== v) { 503 StyleUtil.setElementClassRaw(element, v); 504 } 505 } 506 507 /** 508 * @param {HTMLElement} element 509 * @param {Object} properties 510 * @param {string} selector 511 * @param {HTMLStyleElement} style 512 * @return {HTMLStyleElement} 513 */ 514 applyElementStyle(element, properties, selector, style) { 515 // calculate cssText to apply 516 let cssText = style ? style.textContent || '' : 517 this.transformStyles(element, properties, selector); 518 // if shady and we have a cached style that is not style, decrement 519 let styleInfo = StyleInfo.get(element); 520 let s = styleInfo.customStyle; 521 if (s && !nativeShadow && (s !== style)) { 522 s['_useCount']--; 523 if (s['_useCount'] <= 0 && s.parentNode) { 524 s.parentNode.removeChild(s); 525 } 526 } 527 // apply styling always under native or if we generated style 528 // or the cached style is not in document(!) 529 if (nativeShadow) { 530 // update existing style only under native 531 if (styleInfo.customStyle) { 532 styleInfo.customStyle.textContent = cssText; 533 style = styleInfo.customStyle; 534 // otherwise, if we have css to apply, do so 535 } else if (cssText) { 536 // apply css after the scope style of the element to help with 537 // style precedence rules. 538 style = StyleUtil.applyCss(cssText, selector, element.shadowRoot, 539 styleInfo.placeholder); 540 } 541 } else { 542 // shady and no cache hit 543 if (!style) { 544 // apply css after the scope style of the element to help with 545 // style precedence rules. 546 if (cssText) { 547 style = StyleUtil.applyCss(cssText, selector, null, 548 styleInfo.placeholder); 549 } 550 // shady and cache hit but not in document 551 } else if (!style.parentNode) { 552 if (IS_IE && cssText.indexOf('@media') > -1) { 553 // @media rules may be stale in IE 10 and 11 554 // refresh the text content of the style to revalidate them. 555 style.textContent = cssText; 556 } 557 StyleUtil.applyStyle(style, null, styleInfo.placeholder); 558 } 559 } 560 // ensure this style is our custom style and increment its use count. 561 if (style) { 562 style['_useCount'] = style['_useCount'] || 0; 563 // increment use count if we changed styles 564 if (styleInfo.customStyle != style) { 565 style['_useCount']++; 566 } 567 styleInfo.customStyle = style; 568 } 569 return style; 570 } 571 572 /** 573 * @param {Element} style 574 * @param {Object} properties 575 */ 576 applyCustomStyle(style, properties) { 577 let rules = StyleUtil.rulesForStyle(/** @type {HTMLStyleElement} */(style)); 578 let self = this; 579 style.textContent = StyleUtil.toCssText(rules, function(/** StyleNode */rule) { 580 let css = rule['cssText'] = rule['parsedCssText']; 581 if (rule.propertyInfo && rule.propertyInfo.cssText) { 582 // remove property assignments 583 // so next function isn't confused 584 // NOTE: we have 3 categories of css: 585 // (1) normal properties, 586 // (2) custom property assignments (--foo: red;), 587 // (3) custom property usage: border: var(--foo); @apply(--foo); 588 // In elements, 1 and 3 are separated for efficiency; here they 589 // are not and this makes this case unique. 590 css = removeCustomPropAssignment(/** @type {string} */(css)); 591 // replace with reified properties, scenario is same as mixin 592 rule['cssText'] = self.valueForProperties(css, properties); 593 } 594 }); 595 } 596} 597 598/** 599 * @param {number} n 600 * @param {Array<number>} bits 601 */ 602function addToBitMask(n, bits) { 603 let o = parseInt(n / 32, 10); 604 let v = 1 << (n % 32); 605 bits[o] = (bits[o] || 0) | v; 606} 607 608export default new StyleProperties();