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