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 {nativeShadow, nativeCssVariables, cssBuild} from './style-settings.js';
14import {parse, stringify, types, StyleNode} from './css-parse.js'; // eslint-disable-line no-unused-vars
15import {MEDIA_MATCH} from './common-regex.js';
16import {processUnscopedStyle, isUnscopedStyle} from './unscoped-style-handler.js';
17
18/**
19 * @param {string|StyleNode} rules
20 * @param {function(StyleNode)=} callback
21 * @return {string}
22 */
23export function toCssText (rules, callback) {
24  if (!rules) {
25    return '';
26  }
27  if (typeof rules === 'string') {
28    rules = parse(rules);
29  }
30  if (callback) {
31    forEachRule(rules, callback);
32  }
33  return stringify(rules, nativeCssVariables);
34}
35
36/**
37 * @param {HTMLStyleElement} style
38 * @return {StyleNode}
39 */
40export function rulesForStyle(style) {
41  if (!style['__cssRules'] && style.textContent) {
42    style['__cssRules'] = parse(style.textContent);
43  }
44  return style['__cssRules'] || null;
45}
46
47// Tests if a rule is a keyframes selector, which looks almost exactly
48// like a normal selector but is not (it has nothing to do with scoping
49// for example).
50/**
51 * @param {StyleNode} rule
52 * @return {boolean}
53 */
54export function isKeyframesSelector(rule) {
55  return Boolean(rule['parent']) &&
56  rule['parent']['type'] === types.KEYFRAMES_RULE;
57}
58
59/**
60 * @param {StyleNode} node
61 * @param {Function=} styleRuleCallback
62 * @param {Function=} keyframesRuleCallback
63 * @param {boolean=} onlyActiveRules
64 */
65export function forEachRule(node, styleRuleCallback, keyframesRuleCallback, onlyActiveRules) {
66  if (!node) {
67    return;
68  }
69  let skipRules = false;
70  let type = node['type'];
71  if (onlyActiveRules) {
72    if (type === types.MEDIA_RULE) {
73      let matchMedia = node['selector'].match(MEDIA_MATCH);
74      if (matchMedia) {
75        // if rule is a non matching @media rule, skip subrules
76        if (!window.matchMedia(matchMedia[1]).matches) {
77          skipRules = true;
78        }
79      }
80    }
81  }
82  if (type === types.STYLE_RULE) {
83    styleRuleCallback(node);
84  } else if (keyframesRuleCallback &&
85    type === types.KEYFRAMES_RULE) {
86    keyframesRuleCallback(node);
87  } else if (type === types.MIXIN_RULE) {
88    skipRules = true;
89  }
90  let r$ = node['rules'];
91  if (r$ && !skipRules) {
92    for (let i=0, l=r$.length, r; (i<l) && (r=r$[i]); i++) {
93      forEachRule(r, styleRuleCallback, keyframesRuleCallback, onlyActiveRules);
94    }
95  }
96}
97
98// add a string of cssText to the document.
99/**
100 * @param {string} cssText
101 * @param {string} moniker
102 * @param {Node} target
103 * @param {Node} contextNode
104 * @return {HTMLStyleElement}
105 */
106export function applyCss(cssText, moniker, target, contextNode) {
107  let style = createScopeStyle(cssText, moniker);
108  applyStyle(style, target, contextNode);
109  return style;
110}
111
112/**
113 * @param {string} cssText
114 * @param {string} moniker
115 * @return {HTMLStyleElement}
116 */
117export function createScopeStyle(cssText, moniker) {
118  let style = /** @type {HTMLStyleElement} */(document.createElement('style'));
119  if (moniker) {
120    style.setAttribute('scope', moniker);
121  }
122  style.textContent = cssText;
123  return style;
124}
125
126/**
127 * Track the position of the last added style for placing placeholders
128 * @type {Node}
129 */
130let lastHeadApplyNode = null;
131
132// insert a comment node as a styling position placeholder.
133/**
134 * @param {string} moniker
135 * @return {!Comment}
136 */
137export function applyStylePlaceHolder(moniker) {
138  let placeHolder = document.createComment(' Shady DOM styles for ' +
139    moniker + ' ');
140  let after = lastHeadApplyNode ?
141    lastHeadApplyNode['nextSibling'] : null;
142  let scope = document.head;
143  scope.insertBefore(placeHolder, after || scope.firstChild);
144  lastHeadApplyNode = placeHolder;
145  return placeHolder;
146}
147
148/**
149 * @param {HTMLStyleElement} style
150 * @param {?Node} target
151 * @param {?Node} contextNode
152 */
153export function applyStyle(style, target, contextNode) {
154  target = target || document.head;
155  let after = (contextNode && contextNode.nextSibling) ||
156    target.firstChild;
157  target.insertBefore(style, after);
158  if (!lastHeadApplyNode) {
159    lastHeadApplyNode = style;
160  } else {
161    // only update lastHeadApplyNode if the new style is inserted after the old lastHeadApplyNode
162    let position = style.compareDocumentPosition(lastHeadApplyNode);
163    if (position === Node.DOCUMENT_POSITION_PRECEDING) {
164      lastHeadApplyNode = style;
165    }
166  }
167}
168
169/**
170 * @param {string} buildType
171 * @return {boolean}
172 */
173export function isTargetedBuild(buildType) {
174  return nativeShadow ? buildType === 'shadow' : buildType === 'shady';
175}
176
177/**
178 * Walk from text[start] matching parens and
179 * returns position of the outer end paren
180 * @param {string} text
181 * @param {number} start
182 * @return {number}
183 */
184export function findMatchingParen(text, start) {
185  let level = 0;
186  for (let i=start, l=text.length; i < l; i++) {
187    if (text[i] === '(') {
188      level++;
189    } else if (text[i] === ')') {
190      if (--level === 0) {
191        return i;
192      }
193    }
194  }
195  return -1;
196}
197
198/**
199 * @param {string} str
200 * @param {function(string, string, string, string)} callback
201 */
202export function processVariableAndFallback(str, callback) {
203  // find 'var('
204  let start = str.indexOf('var(');
205  if (start === -1) {
206    // no var?, everything is prefix
207    return callback(str, '', '', '');
208  }
209  //${prefix}var(${inner})${suffix}
210  let end = findMatchingParen(str, start + 3);
211  let inner = str.substring(start + 4, end);
212  let prefix = str.substring(0, start);
213  // suffix may have other variables
214  let suffix = processVariableAndFallback(str.substring(end + 1), callback);
215  let comma = inner.indexOf(',');
216  // value and fallback args should be trimmed to match in property lookup
217  if (comma === -1) {
218    // variable, no fallback
219    return callback(prefix, inner.trim(), '', suffix);
220  }
221  // var(${value},${fallback})
222  let value = inner.substring(0, comma).trim();
223  let fallback = inner.substring(comma + 1).trim();
224  return callback(prefix, value, fallback, suffix);
225}
226
227/**
228 * @param {Element} element
229 * @param {string} value
230 */
231export function setElementClassRaw(element, value) {
232  // use native setAttribute provided by ShadyDOM when setAttribute is patched
233  if (nativeShadow) {
234    element.setAttribute('class', value);
235  } else {
236    window['ShadyDOM']['nativeMethods']['setAttribute'].call(element, 'class', value);
237  }
238}
239
240export const wrap = window['ShadyDOM'] && window['ShadyDOM']['wrap'] || ((node) => node);
241
242/**
243 * @param {Element | {is: string, extends: string}} element
244 * @return {{is: string, typeExtension: string}}
245 */
246export function getIsExtends(element) {
247  let localName = element['localName'];
248  let is = '', typeExtension = '';
249  /*
250  NOTE: technically, this can be wrong for certain svg elements
251  with `-` in the name like `<font-face>`
252  */
253  if (localName) {
254    if (localName.indexOf('-') > -1) {
255      is = localName;
256    } else {
257      typeExtension = localName;
258      is = (element.getAttribute && element.getAttribute('is')) || '';
259    }
260  } else {
261    is = /** @type {?} */(element).is;
262    typeExtension = /** @type {?} */(element).extends;
263  }
264  return {is, typeExtension};
265}
266
267/**
268 * @param {Element|DocumentFragment} element
269 * @return {string}
270 */
271export function gatherStyleText(element) {
272  /** @type {!Array<string>} */
273  const styleTextParts = [];
274  const styles = /** @type {!NodeList<!HTMLStyleElement>} */(element.querySelectorAll('style'));
275  for (let i = 0; i < styles.length; i++) {
276    const style = styles[i];
277    if (isUnscopedStyle(style)) {
278      if (!nativeShadow) {
279        processUnscopedStyle(style);
280        style.parentNode.removeChild(style);
281      }
282    } else {
283      styleTextParts.push(style.textContent);
284      style.parentNode.removeChild(style);
285    }
286  }
287  return styleTextParts.join('').trim();
288}
289
290/**
291 * Split a selector separated by commas into an array in a smart way
292 * @param {string} selector
293 * @return {!Array<string>}
294 */
295export function splitSelectorList(selector) {
296  const parts = [];
297  let part = '';
298  for (let i = 0; i >= 0 && i < selector.length; i++) {
299    // A selector with parentheses will be one complete part
300    if (selector[i] === '(') {
301      // find the matching paren
302      const end = findMatchingParen(selector, i);
303      // push the paren block into the part
304      part += selector.slice(i, end + 1);
305      // move the index to after the paren block
306      i = end;
307    } else if (selector[i] === ',') {
308      parts.push(part);
309      part = '';
310    } else {
311      part += selector[i];
312    }
313  }
314  // catch any pieces after the last comma
315  if (part) {
316    parts.push(part);
317  }
318  return parts;
319}
320
321const CSS_BUILD_ATTR = 'css-build';
322
323/**
324 * Return the polymer-css-build "build type" applied to this element
325 *
326 * @param {!HTMLElement} element
327 * @return {string} Can be "", "shady", or "shadow"
328 */
329export function getCssBuild(element) {
330  if (cssBuild !== undefined) {
331    return /** @type {string} */(cssBuild);
332  }
333  if (element.__cssBuild === undefined) {
334    // try attribute first, as it is the common case
335    const attrValue = element.getAttribute(CSS_BUILD_ATTR);
336    if (attrValue) {
337      element.__cssBuild = attrValue;
338    } else {
339      const buildComment = getBuildComment(element);
340      if (buildComment !== '') {
341        // remove build comment so it is not needlessly copied into every element instance
342        removeBuildComment(element);
343      }
344      element.__cssBuild = buildComment;
345    }
346  }
347  return element.__cssBuild || '';
348}
349
350/**
351 * Check if the given element, either a <template> or <style>, has been processed
352 * by polymer-css-build.
353 *
354 * If so, then we can make a number of optimizations:
355 * - polymer-css-build will decompose mixins into individual CSS Custom Properties,
356 * so the ApplyShim can be skipped entirely.
357 * - Under native ShadowDOM, the style text can just be copied into each instance
358 * without modification
359 * - If the build is "shady" and ShadyDOM is in use, the styling does not need
360 * scoping beyond the shimming of CSS Custom Properties
361 *
362 * @param {!HTMLElement} element
363 * @return {boolean}
364 */
365export function elementHasBuiltCss(element) {
366  return getCssBuild(element) !== '';
367}
368
369/**
370 * For templates made with tagged template literals, polymer-css-build will
371 * insert a comment of the form `<!--css-build:shadow-->`
372 *
373 * @param {!HTMLElement} element
374 * @return {string}
375 */
376export function getBuildComment(element) {
377  const buildComment = element.localName === 'template' ?
378      /** @type {!HTMLTemplateElement} */ (element).content.firstChild :
379      element.firstChild;
380  if (buildComment instanceof Comment) {
381    const commentParts = buildComment.textContent.trim().split(':');
382    if (commentParts[0] === CSS_BUILD_ATTR) {
383      return commentParts[1];
384    }
385  }
386  return '';
387}
388
389/**
390 * Check if the css build status is optimal, and do no unneeded work.
391 *
392 * @param {string=} cssBuild CSS build status
393 * @return {boolean} css build is optimal or not
394 */
395export function isOptimalCssBuild(cssBuild = '') {
396  // CSS custom property shim always requires work
397  if (cssBuild === '' || !nativeCssVariables) {
398    return false;
399  }
400  return nativeShadow ? cssBuild === 'shadow' : cssBuild === 'shady';
401}
402
403/**
404 * @param {!HTMLElement} element
405 */
406function removeBuildComment(element) {
407  const buildComment = element.localName === 'template' ?
408      /** @type {!HTMLTemplateElement} */ (element).content.firstChild :
409      element.firstChild;
410  buildComment.parentNode.removeChild(buildComment);
411}
412