• 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 {StyleNode} from './css-parse.js'; // eslint-disable-line no-unused-vars
14import * as StyleUtil from './style-util.js';
15import {nativeShadow} from './style-settings.js';
16
17/* Transforms ShadowDOM styling into ShadyDOM styling
18
19* scoping:
20
21  * elements in scope get scoping selector class="x-foo-scope"
22  * selectors re-written as follows:
23
24    div button -> div.x-foo-scope button.x-foo-scope
25
26* :host -> scopeName
27
28* :host(...) -> scopeName...
29
30* ::slotted(...) -> scopeName > ...
31
32* ...:dir(ltr|rtl) -> [dir="ltr|rtl"] ..., ...[dir="ltr|rtl"]
33
34* :host(:dir[rtl]) -> scopeName:dir(rtl) -> [dir="rtl"] scopeName, scopeName[dir="rtl"]
35
36*/
37const SCOPE_NAME = 'style-scope';
38
39class StyleTransformer {
40  get SCOPE_NAME() {
41    return SCOPE_NAME;
42  }
43  /**
44   * Given a node and scope name, add a scoping class to each node
45   * in the tree. This facilitates transforming css into scoped rules.
46   * @param {!Node} node
47   * @param {string} scope
48   * @param {boolean=} shouldRemoveScope
49   * @deprecated
50   */
51  dom(node, scope, shouldRemoveScope) {
52    const fn = (node) => {
53      this.element(node, scope || '', shouldRemoveScope);
54    };
55    this._transformDom(node, fn);
56  }
57
58  /**
59   * Given a node and scope name, add a scoping class to each node in the tree.
60   * @param {!Node} node
61   * @param {string} scope
62   */
63  domAddScope(node, scope) {
64    const fn = (node) => {
65      this.element(node, scope || '');
66    };
67    this._transformDom(node, fn);
68  }
69
70  /**
71   * @param {!Node} startNode
72   * @param {!function(!Node)} transformer
73   */
74  _transformDom(startNode, transformer) {
75    if (startNode.nodeType === Node.ELEMENT_NODE) {
76      transformer(startNode)
77    }
78    let c$;
79    if (startNode.localName === 'template') {
80      const template = /** @type {!HTMLTemplateElement} */ (startNode);
81      // In case the template is in svg context, fall back to the node
82      // since it won't be an HTMLTemplateElement with a .content property
83      c$ = (template.content || template._content || template).childNodes;
84    } else {
85      c$ = /** @type {!ParentNode} */ (startNode).children ||
86          startNode.childNodes;
87    }
88    if (c$) {
89      for (let i = 0; i < c$.length; i++) {
90        this._transformDom(c$[i], transformer);
91      }
92    }
93  }
94
95  /**
96   * @param {?} element
97   * @param {?} scope
98   * @param {?=} shouldRemoveScope
99   */
100  element(element, scope, shouldRemoveScope) {
101    // note: if using classes, we add both the general 'style-scope' class
102    // as well as the specific scope. This enables easy filtering of all
103    // `style-scope` elements
104    if (scope) {
105      // note: svg on IE does not have classList so fallback to class
106      if (element.classList) {
107        if (shouldRemoveScope) {
108          element.classList.remove(SCOPE_NAME);
109          element.classList.remove(scope);
110        } else {
111          element.classList.add(SCOPE_NAME);
112          element.classList.add(scope);
113        }
114      } else if (element.getAttribute) {
115        let c = element.getAttribute(CLASS);
116        if (shouldRemoveScope) {
117          if (c) {
118            let newValue = c.replace(SCOPE_NAME, '').replace(scope, '');
119            StyleUtil.setElementClassRaw(element, newValue);
120          }
121        } else {
122          let newValue = (c ? c + ' ' : '') + SCOPE_NAME + ' ' + scope;
123          StyleUtil.setElementClassRaw(element, newValue);
124        }
125      }
126    }
127  }
128
129  /**
130   * Given a node, replace the scoping class to each subnode in the tree.
131   * @param {!Node} node
132   * @param {string} oldScope
133   * @param {string} newScope
134   */
135  domReplaceScope(node, oldScope, newScope) {
136    const fn = (node) => {
137      this.element(node, oldScope, true);
138      this.element(node, newScope);
139    };
140    this._transformDom(node, fn);
141  }
142  /**
143   * Given a node, remove the scoping class to each subnode in the tree.
144   * @param {!Node} node
145   * @param {string} oldScope
146   */
147  domRemoveScope(node, oldScope) {
148    const fn = (node) => {
149      this.element(node, oldScope || '', true);
150    };
151    this._transformDom(node, fn);
152  }
153
154  /**
155   * @param {?} element
156   * @param {?} styleRules
157   * @param {?=} callback
158   * @param {string=} cssBuild
159   * @param {string=} cssText
160   * @return {string}
161   */
162  elementStyles(element, styleRules, callback, cssBuild = '', cssText = '') {
163    // no need to shim selectors if settings.useNativeShadow, also
164    // a shady css build will already have transformed selectors
165    // NOTE: This method may be called as part of static or property shimming.
166    // When there is a targeted build it will not be called for static shimming,
167    // but when the property shim is used it is called and should opt out of
168    // static shimming work when a proper build exists.
169    if (cssText === '') {
170      if (nativeShadow || cssBuild === 'shady') {
171        cssText = StyleUtil.toCssText(styleRules, callback);
172      } else {
173        let {is, typeExtension} = StyleUtil.getIsExtends(element);
174        cssText = this.css(styleRules, is, typeExtension, callback) + '\n\n';
175      }
176    }
177    return cssText.trim();
178  }
179
180  // Given a string of cssText and a scoping string (scope), returns
181  // a string of scoped css where each selector is transformed to include
182  // a class created from the scope. ShadowDOM selectors are also transformed
183  // (e.g. :host) to use the scoping selector.
184  css(rules, scope, ext, callback) {
185    let hostScope = this._calcHostScope(scope, ext);
186    scope = this._calcElementScope(scope);
187    let self = this;
188    return StyleUtil.toCssText(rules, function(/** StyleNode */rule) {
189      if (!rule.isScoped) {
190        self.rule(rule, scope, hostScope);
191        rule.isScoped = true;
192      }
193      if (callback) {
194        callback(rule, scope, hostScope);
195      }
196    });
197  }
198
199  _calcElementScope(scope) {
200    if (scope) {
201      return CSS_CLASS_PREFIX + scope;
202    } else {
203      return '';
204    }
205  }
206
207  _calcHostScope(scope, ext) {
208    return ext ? `[is=${scope}]` : scope;
209  }
210
211  rule(rule, scope, hostScope) {
212    this._transformRule(rule, this._transformComplexSelector,
213      scope, hostScope);
214  }
215
216  /**
217   * transforms a css rule to a scoped rule.
218   *
219   * @param {StyleNode} rule
220   * @param {Function} transformer
221   * @param {string=} scope
222   * @param {string=} hostScope
223   */
224  _transformRule(rule, transformer, scope, hostScope) {
225    // NOTE: save transformedSelector for subsequent matching of elements
226    // against selectors (e.g. when calculating style properties)
227    rule['selector'] = rule.transformedSelector =
228      this._transformRuleCss(rule, transformer, scope, hostScope);
229  }
230
231  /**
232   * @param {StyleNode} rule
233   * @param {Function} transformer
234   * @param {string=} scope
235   * @param {string=} hostScope
236   */
237  _transformRuleCss(rule, transformer, scope, hostScope) {
238    let p$ = StyleUtil.splitSelectorList(rule['selector']);
239    // we want to skip transformation of rules that appear in keyframes,
240    // because they are keyframe selectors, not element selectors.
241    if (!StyleUtil.isKeyframesSelector(rule)) {
242      for (let i=0, l=p$.length, p; (i<l) && (p=p$[i]); i++) {
243        p$[i] = transformer.call(this, p, scope, hostScope);
244      }
245    }
246    return p$.filter((part) => Boolean(part)).join(COMPLEX_SELECTOR_SEP);
247  }
248
249  /**
250   * @param {string} selector
251   * @return {string}
252   */
253  _twiddleNthPlus(selector) {
254    return selector.replace(NTH, (m, type, inside) => {
255      if (inside.indexOf('+') > -1) {
256        inside = inside.replace(/\+/g, '___');
257      } else if (inside.indexOf('___') > -1) {
258        inside = inside.replace(/___/g, '+');
259      }
260      return `:${type}(${inside})`;
261    });
262  }
263
264  /**
265   * Preserve `:matches()` selectors by replacing them with MATCHES_REPLACMENT
266   * and returning an array of `:matches()` selectors.
267   * Use `_replacesMatchesPseudo` to replace the `:matches()` parts
268   *
269   * @param {string} selector
270   * @return {{selector: string, matches: !Array<string>}}
271   */
272  _preserveMatchesPseudo(selector) {
273    /** @type {!Array<string>} */
274    const matches = [];
275    let match;
276    while ((match = selector.match(MATCHES))) {
277      const start = match.index;
278      const end = StyleUtil.findMatchingParen(selector, start);
279      if (end === -1) {
280        throw new Error(`${match.input} selector missing ')'`)
281      }
282      const part = selector.slice(start, end + 1);
283      selector = selector.replace(part, MATCHES_REPLACEMENT);
284      matches.push(part);
285    }
286    return {selector, matches};
287  }
288
289  /**
290   * Replace MATCHES_REPLACMENT character with the given set of `:matches()`
291   * selectors.
292   *
293   * @param {string} selector
294   * @param {!Array<string>} matches
295   * @return {string}
296   */
297  _replaceMatchesPseudo(selector, matches) {
298    const parts = selector.split(MATCHES_REPLACEMENT);
299    return matches.reduce((acc, cur, idx) => acc + cur + parts[idx + 1], parts[0]);
300  }
301
302/**
303 * @param {string} selector
304 * @param {string} scope
305 * @param {string=} hostScope
306 */
307  _transformComplexSelector(selector, scope, hostScope) {
308    let stop = false;
309    selector = selector.trim();
310    // Remove spaces inside of selectors like `:nth-of-type` because it confuses SIMPLE_SELECTOR_SEP
311    let isNth = NTH.test(selector);
312    if (isNth) {
313      selector = selector.replace(NTH, (m, type, inner) => `:${type}(${inner.replace(/\s/g, '')})`)
314      selector = this._twiddleNthPlus(selector);
315    }
316    // Preserve selectors like `:-webkit-any` so that SIMPLE_SELECTOR_SEP does
317    // not get confused by spaces inside the pseudo selector
318    const isMatches = MATCHES.test(selector);
319    /** @type {!Array<string>} */
320    let matches;
321    if (isMatches) {
322      ({selector, matches} = this._preserveMatchesPseudo(selector));
323    }
324    selector = selector.replace(SLOTTED_START, `${HOST} $1`);
325    selector = selector.replace(SIMPLE_SELECTOR_SEP, (m, c, s) => {
326      if (!stop) {
327        let info = this._transformCompoundSelector(s, c, scope, hostScope);
328        stop = stop || info.stop;
329        c = info.combinator;
330        s = info.value;
331      }
332      return c + s;
333    });
334    // replace `:matches()` selectors
335    if (isMatches) {
336      selector = this._replaceMatchesPseudo(selector, matches);
337    }
338    if (isNth) {
339      selector = this._twiddleNthPlus(selector);
340    }
341    return selector;
342  }
343
344  _transformCompoundSelector(selector, combinator, scope, hostScope) {
345    // replace :host with host scoping class
346    let slottedIndex = selector.indexOf(SLOTTED);
347    if (selector.indexOf(HOST) >= 0) {
348      selector = this._transformHostSelector(selector, hostScope);
349    // replace other selectors with scoping class
350    } else if (slottedIndex !== 0) {
351      selector = scope ? this._transformSimpleSelector(selector, scope) :
352        selector;
353    }
354    // mark ::slotted() scope jump to replace with descendant selector + arg
355    // also ignore left-side combinator
356    let slotted = false;
357    if (slottedIndex >= 0) {
358      combinator = '';
359      slotted = true;
360    }
361    // process scope jumping selectors up to the scope jump and then stop
362    let stop;
363    if (slotted) {
364      stop = true;
365      if (slotted) {
366        // .zonk ::slotted(.foo) -> .zonk.scope > .foo
367        selector = selector.replace(SLOTTED_PAREN, (m, paren) => ` > ${paren}`);
368      }
369    }
370    selector = selector.replace(DIR_PAREN, (m, before, dir) =>
371      `[dir="${dir}"] ${before}, ${before}[dir="${dir}"]`);
372    return {value: selector, combinator, stop};
373  }
374
375  _transformSimpleSelector(selector, scope) {
376    const attributes = selector.split(/(\[.+?\])/);
377
378    const output = [];
379    for (let i = 0; i < attributes.length; i++) {
380      // Do not attempt to transform any attribute selector content
381      if ((i % 2) === 1) {
382        output.push(attributes[i]);
383      } else {
384        const part = attributes[i];
385
386        if (!(part === '' && i === attributes.length - 1)) {
387          let p$ = part.split(PSEUDO_PREFIX);
388          p$[0] += scope;
389          output.push(p$.join(PSEUDO_PREFIX));
390        }
391      }
392    }
393
394    return output.join('');
395  }
396
397  // :host(...) -> scopeName...
398  _transformHostSelector(selector, hostScope) {
399    let m = selector.match(HOST_PAREN);
400    let paren = m && m[2].trim() || '';
401    if (paren) {
402      if (!paren[0].match(SIMPLE_SELECTOR_PREFIX)) {
403        // paren starts with a type selector
404        let typeSelector = paren.split(SIMPLE_SELECTOR_PREFIX)[0];
405        // if the type selector is our hostScope then avoid pre-pending it
406        if (typeSelector === hostScope) {
407          return paren;
408        // otherwise, this selector should not match in this scope so
409        // output a bogus selector.
410        } else {
411          return SELECTOR_NO_MATCH;
412        }
413      } else {
414        // make sure to do a replace here to catch selectors like:
415        // `:host(.foo)::before`
416        return selector.replace(HOST_PAREN, function(m, host, paren) {
417          return hostScope + paren;
418        });
419      }
420    // if no paren, do a straight :host replacement.
421    // TODO(sorvell): this should not strictly be necessary but
422    // it's needed to maintain support for `:host[foo]` type selectors
423    // which have been improperly used under Shady DOM. This should be
424    // deprecated.
425    } else {
426      return selector.replace(HOST, hostScope);
427    }
428  }
429
430  /**
431   * @param {StyleNode} rule
432   */
433  documentRule(rule) {
434    // reset selector in case this is redone.
435    rule['selector'] = rule['parsedSelector'];
436    this.normalizeRootSelector(rule);
437    this._transformRule(rule, this._transformDocumentSelector);
438  }
439
440  /**
441   * @param {StyleNode} rule
442   */
443  normalizeRootSelector(rule) {
444    if (rule['selector'] === ROOT) {
445      rule['selector'] = 'html';
446    }
447  }
448
449/**
450 * @param {string} selector
451 */
452  _transformDocumentSelector(selector) {
453    if (selector.match(HOST)) {
454      // remove ':host' type selectors in document rules
455      return '';
456    } else if (selector.match(SLOTTED)) {
457      return this._transformComplexSelector(selector, SCOPE_DOC_SELECTOR)
458    } else {
459      return this._transformSimpleSelector(selector.trim(), SCOPE_DOC_SELECTOR);
460    }
461  }
462}
463
464const NTH = /:(nth[-\w]+)\(([^)]+)\)/;
465const SCOPE_DOC_SELECTOR = `:not(.${SCOPE_NAME})`;
466const COMPLEX_SELECTOR_SEP = ',';
467const SIMPLE_SELECTOR_SEP = /(^|[\s>+~]+)((?:\[.+?\]|[^\s>+~=[])+)/g;
468const SIMPLE_SELECTOR_PREFIX = /[[.:#*]/;
469const HOST = ':host';
470const ROOT = ':root';
471const SLOTTED = '::slotted';
472const SLOTTED_START = new RegExp(`^(${SLOTTED})`);
473// NOTE: this supports 1 nested () pair for things like
474// :host(:not([selected]), more general support requires
475// parsing which seems like overkill
476const HOST_PAREN = /(:host)(?:\(((?:\([^)(]*\)|[^)(]*)+?)\))/;
477// similar to HOST_PAREN
478const SLOTTED_PAREN = /(?:::slotted)(?:\(((?:\([^)(]*\)|[^)(]*)+?)\))/;
479const DIR_PAREN = /(.*):dir\((?:(ltr|rtl))\)/;
480const CSS_CLASS_PREFIX = '.';
481const PSEUDO_PREFIX = ':';
482const CLASS = 'class';
483const SELECTOR_NO_MATCH = 'should_not_match';
484const MATCHES = /:(?:matches|any|-(?:webkit|moz)-any)/;
485const MATCHES_REPLACEMENT = '\u{e000}';
486
487export default new StyleTransformer()
488