• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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();