| OLD | NEW |
| (Empty) | |
| 1 /** |
| 2 @license |
| 3 Copyright (c) 2017 The Polymer Project Authors. All rights reserved. |
| 4 This code may only be used under the BSD style license found at http://polymer.g
ithub.io/LICENSE.txt |
| 5 The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt |
| 6 The complete set of contributors may be found at http://polymer.github.io/CONTRI
BUTORS.txt |
| 7 Code distributed by Google as part of the polymer project is also |
| 8 subject to an additional IP rights grant found at http://polymer.github.io/PATEN
TS.txt |
| 9 */ |
| 10 /* |
| 11 * The apply shim simulates the behavior of `@apply` proposed at |
| 12 * https://tabatkins.github.io/specs/css-apply-rule/. |
| 13 * The approach is to convert a property like this: |
| 14 * |
| 15 * --foo: {color: red; background: blue;} |
| 16 * |
| 17 * to this: |
| 18 * |
| 19 * --foo_-_color: red; |
| 20 * --foo_-_background: blue; |
| 21 * |
| 22 * Then where `@apply --foo` is used, that is converted to: |
| 23 * |
| 24 * color: var(--foo_-_color); |
| 25 * background: var(--foo_-_background); |
| 26 * |
| 27 * This approach generally works but there are some issues and limitations. |
| 28 * Consider, for example, that somewhere *between* where `--foo` is set and used
, |
| 29 * another element sets it to: |
| 30 * |
| 31 * --foo: { border: 2px solid red; } |
| 32 * |
| 33 * We must now ensure that the color and background from the previous setting |
| 34 * do not apply. This is accomplished by changing the property set to this: |
| 35 * |
| 36 * --foo_-_border: 2px solid red; |
| 37 * --foo_-_color: initial; |
| 38 * --foo_-_background: initial; |
| 39 * |
| 40 * This works but introduces one new issue. |
| 41 * Consider this setup at the point where the `@apply` is used: |
| 42 * |
| 43 * background: orange; |
| 44 * `@apply` --foo; |
| 45 * |
| 46 * In this case the background will be unset (initial) rather than the desired |
| 47 * `orange`. We address this by altering the property set to use a fallback |
| 48 * value like this: |
| 49 * |
| 50 * color: var(--foo_-_color); |
| 51 * background: var(--foo_-_background, orange); |
| 52 * border: var(--foo_-_border); |
| 53 * |
| 54 * Note that the default is retained in the property set and the `background` is |
| 55 * the desired `orange`. This leads us to a limitation. |
| 56 * |
| 57 * Limitation 1: |
| 58 |
| 59 * Only properties in the rule where the `@apply` |
| 60 * is used are considered as default values. |
| 61 * If another rule matches the element and sets `background` with |
| 62 * less specificity than the rule in which `@apply` appears, |
| 63 * the `background` will not be set. |
| 64 * |
| 65 * Limitation 2: |
| 66 * |
| 67 * When using Polymer's `updateStyles` api, new properties may not be set for |
| 68 * `@apply` properties. |
| 69 |
| 70 */ |
| 71 |
| 72 'use strict'; |
| 73 |
| 74 import {forEachRule, processVariableAndFallback, rulesForStyle, toCssText} from
'./style-util.js' |
| 75 import {MIXIN_MATCH, VAR_ASSIGN} from './common-regex.js' |
| 76 import {detectMixin} from './common-utils.js' |
| 77 import {StyleNode} from './css-parse.js' // eslint-disable-line no-unused-vars |
| 78 |
| 79 const APPLY_NAME_CLEAN = /;\s*/m; |
| 80 const INITIAL_INHERIT = /^\s*(initial)|(inherit)\s*$/; |
| 81 |
| 82 // separator used between mixin-name and mixin-property-name when producing prop
erties |
| 83 // NOTE: plain '-' may cause collisions in user styles |
| 84 const MIXIN_VAR_SEP = '_-_'; |
| 85 |
| 86 /** |
| 87 * @typedef {!Object<string, string>} |
| 88 */ |
| 89 let PropertyEntry; // eslint-disable-line no-unused-vars |
| 90 |
| 91 /** |
| 92 * @typedef {!Object<string, boolean>} |
| 93 */ |
| 94 let DependantsEntry; // eslint-disable-line no-unused-vars |
| 95 |
| 96 /** @typedef {{ |
| 97 * properties: PropertyEntry, |
| 98 * dependants: DependantsEntry |
| 99 * }} |
| 100 */ |
| 101 let MixinMapEntry; // eslint-disable-line no-unused-vars |
| 102 |
| 103 // map of mixin to property names |
| 104 // --foo: {border: 2px} -> {properties: {(--foo, ['border'])}, dependants: {'ele
ment-name': proto}} |
| 105 class MixinMap { |
| 106 constructor() { |
| 107 /** @type {!Object<string, !MixinMapEntry>} */ |
| 108 this._map = {}; |
| 109 } |
| 110 /** |
| 111 * @param {string} name |
| 112 * @param {!PropertyEntry} props |
| 113 */ |
| 114 set(name, props) { |
| 115 name = name.trim(); |
| 116 this._map[name] = { |
| 117 properties: props, |
| 118 dependants: {} |
| 119 } |
| 120 } |
| 121 /** |
| 122 * @param {string} name |
| 123 * @return {MixinMapEntry} |
| 124 */ |
| 125 get(name) { |
| 126 name = name.trim(); |
| 127 return this._map[name] || null; |
| 128 } |
| 129 } |
| 130 |
| 131 /** |
| 132 * Callback for when an element is marked invalid |
| 133 * @type {?function(string)} |
| 134 */ |
| 135 let invalidCallback = null; |
| 136 |
| 137 /** @unrestricted */ |
| 138 class ApplyShim { |
| 139 constructor() { |
| 140 /** @type {?string} */ |
| 141 this._currentElement = null; |
| 142 /** @type {HTMLMetaElement} */ |
| 143 this._measureElement = null; |
| 144 this._map = new MixinMap(); |
| 145 } |
| 146 /** |
| 147 * return true if `cssText` contains a mixin definition or consumption |
| 148 * @param {string} cssText |
| 149 * @return {boolean} |
| 150 */ |
| 151 detectMixin(cssText) { |
| 152 return detectMixin(cssText); |
| 153 } |
| 154 /** |
| 155 * @param {!HTMLTemplateElement} template |
| 156 * @param {string} elementName |
| 157 * @return {StyleNode} |
| 158 */ |
| 159 transformTemplate(template, elementName) { |
| 160 const style = /** @type {HTMLStyleElement} */(template.content.querySelector
('style')); |
| 161 /** @type {StyleNode} */ |
| 162 let ast = null; |
| 163 if (style) { |
| 164 ast = this.transformStyle(style, elementName); |
| 165 } |
| 166 return ast; |
| 167 } |
| 168 /** |
| 169 * @param {!HTMLStyleElement} style |
| 170 * @param {string} elementName |
| 171 * @return {StyleNode} |
| 172 */ |
| 173 transformStyle(style, elementName = '') { |
| 174 let ast = rulesForStyle(style); |
| 175 this.transformRules(ast, elementName); |
| 176 style.textContent = toCssText(ast); |
| 177 return ast; |
| 178 } |
| 179 /** |
| 180 * @param {!HTMLStyleElement} style |
| 181 * @return {StyleNode} |
| 182 */ |
| 183 transformCustomStyle(style) { |
| 184 let ast = rulesForStyle(style); |
| 185 forEachRule(ast, (rule) => { |
| 186 if (rule['selector'] === ':root') { |
| 187 rule['selector'] = 'html'; |
| 188 } |
| 189 this.transformRule(rule); |
| 190 }) |
| 191 style.textContent = toCssText(ast); |
| 192 return ast; |
| 193 } |
| 194 /** |
| 195 * @param {StyleNode} rules |
| 196 * @param {string} elementName |
| 197 */ |
| 198 transformRules(rules, elementName) { |
| 199 this._currentElement = elementName; |
| 200 forEachRule(rules, (r) => { |
| 201 this.transformRule(r); |
| 202 }); |
| 203 this._currentElement = null; |
| 204 } |
| 205 /** |
| 206 * @param {!StyleNode} rule |
| 207 */ |
| 208 transformRule(rule) { |
| 209 rule['cssText'] = this.transformCssText(rule['parsedCssText']); |
| 210 // :root was only used for variable assignment in property shim, |
| 211 // but generates invalid selectors with real properties. |
| 212 // replace with `:host > *`, which serves the same effect |
| 213 if (rule['selector'] === ':root') { |
| 214 rule['selector'] = ':host > *'; |
| 215 } |
| 216 } |
| 217 /** |
| 218 * @param {string} cssText |
| 219 * @return {string} |
| 220 */ |
| 221 transformCssText(cssText) { |
| 222 // produce variables |
| 223 cssText = cssText.replace(VAR_ASSIGN, (matchText, propertyName, valuePropert
y, valueMixin) => |
| 224 this._produceCssProperties(matchText, propertyName, valueProperty, valueMi
xin)); |
| 225 // consume mixins |
| 226 return this._consumeCssProperties(cssText); |
| 227 } |
| 228 /** |
| 229 * @param {string} property |
| 230 * @return {string} |
| 231 */ |
| 232 _getInitialValueForProperty(property) { |
| 233 if (!this._measureElement) { |
| 234 this._measureElement = /** @type {HTMLMetaElement} */(document.createEleme
nt('meta')); |
| 235 this._measureElement.setAttribute('apply-shim-measure', ''); |
| 236 this._measureElement.style.all = 'initial'; |
| 237 document.head.appendChild(this._measureElement); |
| 238 } |
| 239 return window.getComputedStyle(this._measureElement).getPropertyValue(proper
ty); |
| 240 } |
| 241 /** |
| 242 * replace mixin consumption with variable consumption |
| 243 * @param {string} text |
| 244 * @return {string} |
| 245 */ |
| 246 _consumeCssProperties(text) { |
| 247 /** @type {Array} */ |
| 248 let m = null; |
| 249 // loop over text until all mixins with defintions have been applied |
| 250 while((m = MIXIN_MATCH.exec(text))) { |
| 251 let matchText = m[0]; |
| 252 let mixinName = m[1]; |
| 253 let idx = m.index; |
| 254 // collect properties before apply to be "defaults" if mixin might overrid
e them |
| 255 // match includes a "prefix", so find the start and end positions of @appl
y |
| 256 let applyPos = idx + matchText.indexOf('@apply'); |
| 257 let afterApplyPos = idx + matchText.length; |
| 258 // find props defined before this @apply |
| 259 let textBeforeApply = text.slice(0, applyPos); |
| 260 let textAfterApply = text.slice(afterApplyPos); |
| 261 let defaults = this._cssTextToMap(textBeforeApply); |
| 262 let replacement = this._atApplyToCssProperties(mixinName, defaults); |
| 263 // use regex match position to replace mixin, keep linear processing time |
| 264 text = `${textBeforeApply}${replacement}${textAfterApply}`; |
| 265 // move regex search to _after_ replacement |
| 266 MIXIN_MATCH.lastIndex = idx + replacement.length; |
| 267 } |
| 268 return text; |
| 269 } |
| 270 /** |
| 271 * produce variable consumption at the site of mixin consumption |
| 272 * `@apply` --foo; -> for all props (${propname}: var(--foo_-_${propname}, ${f
allback[propname]}})) |
| 273 * Example: |
| 274 * border: var(--foo_-_border); padding: var(--foo_-_padding, 2px) |
| 275 * |
| 276 * @param {string} mixinName |
| 277 * @param {Object} fallbacks |
| 278 * @return {string} |
| 279 */ |
| 280 _atApplyToCssProperties(mixinName, fallbacks) { |
| 281 mixinName = mixinName.replace(APPLY_NAME_CLEAN, ''); |
| 282 let vars = []; |
| 283 let mixinEntry = this._map.get(mixinName); |
| 284 // if we depend on a mixin before it is created |
| 285 // make a sentinel entry in the map to add this element as a dependency for
when it is defined. |
| 286 if (!mixinEntry) { |
| 287 this._map.set(mixinName, {}); |
| 288 mixinEntry = this._map.get(mixinName); |
| 289 } |
| 290 if (mixinEntry) { |
| 291 if (this._currentElement) { |
| 292 mixinEntry.dependants[this._currentElement] = true; |
| 293 } |
| 294 let p, parts, f; |
| 295 for (p in mixinEntry.properties) { |
| 296 f = fallbacks && fallbacks[p]; |
| 297 parts = [p, ': var(', mixinName, MIXIN_VAR_SEP, p]; |
| 298 if (f) { |
| 299 parts.push(',', f); |
| 300 } |
| 301 parts.push(')'); |
| 302 vars.push(parts.join('')); |
| 303 } |
| 304 } |
| 305 return vars.join('; '); |
| 306 } |
| 307 |
| 308 /** |
| 309 * @param {string} property |
| 310 * @param {string} value |
| 311 * @return {string} |
| 312 */ |
| 313 _replaceInitialOrInherit(property, value) { |
| 314 let match = INITIAL_INHERIT.exec(value); |
| 315 if (match) { |
| 316 if (match[1]) { |
| 317 // initial |
| 318 // replace `initial` with the concrete initial value for this property |
| 319 value = this._getInitialValueForProperty(property); |
| 320 } else { |
| 321 // inherit |
| 322 // with this purposfully illegal value, the variable will be invalid at |
| 323 // compute time (https://www.w3.org/TR/css-variables/#invalid-at-compute
d-value-time) |
| 324 // and for inheriting values, will behave similarly |
| 325 // we cannot support the same behavior for non inheriting values like 'b
order' |
| 326 value = 'apply-shim-inherit'; |
| 327 } |
| 328 } |
| 329 return value; |
| 330 } |
| 331 |
| 332 /** |
| 333 * "parse" a mixin definition into a map of properties and values |
| 334 * cssTextToMap('border: 2px solid black') -> ('border', '2px solid black') |
| 335 * @param {string} text |
| 336 * @return {!Object<string, string>} |
| 337 */ |
| 338 _cssTextToMap(text) { |
| 339 let props = text.split(';'); |
| 340 let property, value; |
| 341 let out = {}; |
| 342 for (let i = 0, p, sp; i < props.length; i++) { |
| 343 p = props[i]; |
| 344 if (p) { |
| 345 sp = p.split(':'); |
| 346 // ignore lines that aren't definitions like @media |
| 347 if (sp.length > 1) { |
| 348 property = sp[0].trim(); |
| 349 // some properties may have ':' in the value, like data urls |
| 350 value = this._replaceInitialOrInherit(property, sp.slice(1).join(':'))
; |
| 351 out[property] = value; |
| 352 } |
| 353 } |
| 354 } |
| 355 return out; |
| 356 } |
| 357 |
| 358 /** |
| 359 * @param {MixinMapEntry} mixinEntry |
| 360 */ |
| 361 _invalidateMixinEntry(mixinEntry) { |
| 362 if (!invalidCallback) { |
| 363 return; |
| 364 } |
| 365 for (let elementName in mixinEntry.dependants) { |
| 366 if (elementName !== this._currentElement) { |
| 367 invalidCallback(elementName); |
| 368 } |
| 369 } |
| 370 } |
| 371 |
| 372 /** |
| 373 * @param {string} matchText |
| 374 * @param {string} propertyName |
| 375 * @param {?string} valueProperty |
| 376 * @param {?string} valueMixin |
| 377 * @return {string} |
| 378 */ |
| 379 _produceCssProperties(matchText, propertyName, valueProperty, valueMixin) { |
| 380 // handle case where property value is a mixin |
| 381 if (valueProperty) { |
| 382 // form: --mixin2: var(--mixin1), where --mixin1 is in the map |
| 383 processVariableAndFallback(valueProperty, (prefix, value) => { |
| 384 if (value && this._map.get(value)) { |
| 385 valueMixin = `@apply ${value};` |
| 386 } |
| 387 }); |
| 388 } |
| 389 if (!valueMixin) { |
| 390 return matchText; |
| 391 } |
| 392 let mixinAsProperties = this._consumeCssProperties(valueMixin); |
| 393 let prefix = matchText.slice(0, matchText.indexOf('--')); |
| 394 let mixinValues = this._cssTextToMap(mixinAsProperties); |
| 395 let combinedProps = mixinValues; |
| 396 let mixinEntry = this._map.get(propertyName); |
| 397 let oldProps = mixinEntry && mixinEntry.properties; |
| 398 if (oldProps) { |
| 399 // NOTE: since we use mixin, the map of properties is updated here |
| 400 // and this is what we want. |
| 401 combinedProps = Object.assign(Object.create(oldProps), mixinValues); |
| 402 } else { |
| 403 this._map.set(propertyName, combinedProps); |
| 404 } |
| 405 let out = []; |
| 406 let p, v; |
| 407 // set variables defined by current mixin |
| 408 let needToInvalidate = false; |
| 409 for (p in combinedProps) { |
| 410 v = mixinValues[p]; |
| 411 // if property not defined by current mixin, set initial |
| 412 if (v === undefined) { |
| 413 v = 'initial'; |
| 414 } |
| 415 if (oldProps && !(p in oldProps)) { |
| 416 needToInvalidate = true; |
| 417 } |
| 418 out.push(`${propertyName}${MIXIN_VAR_SEP}${p}: ${v}`); |
| 419 } |
| 420 if (needToInvalidate) { |
| 421 this._invalidateMixinEntry(mixinEntry); |
| 422 } |
| 423 if (mixinEntry) { |
| 424 mixinEntry.properties = combinedProps; |
| 425 } |
| 426 // because the mixinMap is global, the mixin might conflict with |
| 427 // a different scope's simple variable definition: |
| 428 // Example: |
| 429 // some style somewhere: |
| 430 // --mixin1:{ ... } |
| 431 // --mixin2: var(--mixin1); |
| 432 // some other element: |
| 433 // --mixin1: 10px solid red; |
| 434 // --foo: var(--mixin1); |
| 435 // In this case, we leave the original variable definition in place. |
| 436 if (valueProperty) { |
| 437 prefix = `${matchText};${prefix}`; |
| 438 } |
| 439 return `${prefix}${out.join('; ')};`; |
| 440 } |
| 441 } |
| 442 |
| 443 /* exports */ |
| 444 ApplyShim.prototype['detectMixin'] = ApplyShim.prototype.detectMixin; |
| 445 ApplyShim.prototype['transformStyle'] = ApplyShim.prototype.transformStyle; |
| 446 ApplyShim.prototype['transformCustomStyle'] = ApplyShim.prototype.transformCusto
mStyle; |
| 447 ApplyShim.prototype['transformRules'] = ApplyShim.prototype.transformRules; |
| 448 ApplyShim.prototype['transformRule'] = ApplyShim.prototype.transformRule; |
| 449 ApplyShim.prototype['transformTemplate'] = ApplyShim.prototype.transformTemplate
; |
| 450 ApplyShim.prototype['_separator'] = MIXIN_VAR_SEP; |
| 451 Object.defineProperty(ApplyShim.prototype, 'invalidCallback', { |
| 452 /** @return {?function(string)} */ |
| 453 get() { |
| 454 return invalidCallback; |
| 455 }, |
| 456 /** @param {?function(string)} cb */ |
| 457 set(cb) { |
| 458 invalidCallback = cb; |
| 459 } |
| 460 }); |
| 461 |
| 462 export default ApplyShim; |
| OLD | NEW |