UNPKG

webdash-readme-preview

Version:
612 lines (583 loc) 21.9 kB
/** @license Copyright (c) 2017 The Polymer Project Authors. All rights reserved. This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as part of the polymer project is also subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt */ 'use strict'; import {removeCustomPropAssignment, StyleNode} from './css-parse.js'; // eslint-disable-line no-unused-vars import {nativeShadow} from './style-settings.js'; import StyleTransformer from './style-transformer.js'; import * as StyleUtil from './style-util.js'; import * as RX from './common-regex.js'; import StyleInfo from './style-info.js'; // TODO: dedupe with shady /** * @param {string} selector * @return {boolean} * @this {Element} */ const matchesSelector = function(selector) { const method = this.matches || this.matchesSelector || this.mozMatchesSelector || this.msMatchesSelector || this.oMatchesSelector || this.webkitMatchesSelector; return method && method.call(this, selector); }; const IS_IE = navigator.userAgent.match('Trident'); const XSCOPE_NAME = 'x-scope'; class StyleProperties { get XSCOPE_NAME() { return XSCOPE_NAME; } /** * decorates styles with rule info and returns an array of used style property names * * @param {StyleNode} rules * @return {Array<string>} */ decorateStyles(rules) { let self = this, props = {}, keyframes = [], ruleIndex = 0; StyleUtil.forEachRule(rules, function(rule) { self.decorateRule(rule); // mark in-order position of ast rule in styles block, used for cache key rule.index = ruleIndex++; self.collectPropertiesInCssText(rule.propertyInfo.cssText, props); }, function onKeyframesRule(rule) { keyframes.push(rule); }); // Cache all found keyframes rules for later reference: rules._keyframes = keyframes; // return this list of property names *consumes* in these styles. let names = []; for (let i in props) { names.push(i); } return names; } // decorate a single rule with property info decorateRule(rule) { if (rule.propertyInfo) { return rule.propertyInfo; } let info = {}, properties = {}; let hasProperties = this.collectProperties(rule, properties); if (hasProperties) { info.properties = properties; // TODO(sorvell): workaround parser seeing mixins as additional rules rule['rules'] = null; } info.cssText = this.collectCssText(rule); rule.propertyInfo = info; return info; } // collects the custom properties from a rule's cssText collectProperties(rule, properties) { let info = rule.propertyInfo; if (info) { if (info.properties) { Object.assign(properties, info.properties); return true; } } else { let m, rx = RX.VAR_ASSIGN; let cssText = rule['parsedCssText']; let value; let any; while ((m = rx.exec(cssText))) { // note: group 2 is var, 3 is mixin value = (m[2] || m[3]).trim(); // value of 'inherit' or 'unset' is equivalent to not setting the property here if (value !== 'inherit' || value !== 'unset') { properties[m[1].trim()] = value; } any = true; } return any; } } // returns cssText of properties that consume variables/mixins collectCssText(rule) { return this.collectConsumingCssText(rule['parsedCssText']); } // NOTE: we support consumption inside mixin assignment // but not production, so strip out {...} collectConsumingCssText(cssText) { return cssText.replace(RX.BRACKETED, '') .replace(RX.VAR_ASSIGN, ''); } collectPropertiesInCssText(cssText, props) { let m; while ((m = RX.VAR_CONSUMED.exec(cssText))) { let name = m[1]; // This regex catches all variable names, and following non-whitespace char // If next char is not ':', then variable is a consumer if (m[2] !== ':') { props[name] = true; } } } // turns custom properties into realized values. reify(props) { // big perf optimization here: reify only *own* properties // since this object has __proto__ of the element's scope properties let names = Object.getOwnPropertyNames(props); for (let i=0, n; i < names.length; i++) { n = names[i]; props[n] = this.valueForProperty(props[n], props); } } // given a property value, returns the reified value // a property value may be: // (1) a literal value like: red or 5px; // (2) a variable value like: var(--a), var(--a, red), or var(--a, --b) or // var(--a, var(--b)); // (3) a literal mixin value like { properties }. Each of these properties // can have values that are: (a) literal, (b) variables, (c) @apply mixins. valueForProperty(property, props) { // case (1) default // case (3) defines a mixin and we have to reify the internals if (property) { if (property.indexOf(';') >=0) { property = this.valueForProperties(property, props); } else { // case (2) variable let self = this; let fn = function(prefix, value, fallback, suffix) { if (!value) { return prefix + suffix; } let propertyValue = self.valueForProperty(props[value], props); // if value is "initial", then the variable should be treated as unset if (!propertyValue || propertyValue === 'initial') { // fallback may be --a or var(--a) or literal propertyValue = self.valueForProperty(props[fallback] || fallback, props) || fallback; } else if (propertyValue === 'apply-shim-inherit') { // CSS build will replace `inherit` with `apply-shim-inherit` // for use with native css variables. // Since we have full control, we can use `inherit` directly. propertyValue = 'inherit'; } return prefix + (propertyValue || '') + suffix; }; property = StyleUtil.processVariableAndFallback(property, fn); } } return property && property.trim() || ''; } // note: we do not yet support mixin within mixin valueForProperties(property, props) { let parts = property.split(';'); for (let i=0, p, m; i<parts.length; i++) { if ((p = parts[i])) { RX.MIXIN_MATCH.lastIndex = 0; m = RX.MIXIN_MATCH.exec(p); if (m) { p = this.valueForProperty(props[m[1]], props); } else { let colon = p.indexOf(':'); if (colon !== -1) { let pp = p.substring(colon); pp = pp.trim(); pp = this.valueForProperty(pp, props) || pp; p = p.substring(0, colon) + pp; } } parts[i] = (p && p.lastIndexOf(';') === p.length - 1) ? // strip trailing ; p.slice(0, -1) : p || ''; } } return parts.join(';'); } applyProperties(rule, props) { let output = ''; // dynamically added sheets may not be decorated so ensure they are. if (!rule.propertyInfo) { this.decorateRule(rule); } if (rule.propertyInfo.cssText) { output = this.valueForProperties(rule.propertyInfo.cssText, props); } rule['cssText'] = output; } // Apply keyframe transformations to the cssText of a given rule. The // keyframeTransforms object is a map of keyframe names to transformer // functions which take in cssText and spit out transformed cssText. applyKeyframeTransforms(rule, keyframeTransforms) { let input = rule['cssText']; let output = rule['cssText']; if (rule.hasAnimations == null) { // Cache whether or not the rule has any animations to begin with: rule.hasAnimations = RX.ANIMATION_MATCH.test(input); } // If there are no animations referenced, we can skip transforms: if (rule.hasAnimations) { let transform; // If we haven't transformed this rule before, we iterate over all // transforms: if (rule.keyframeNamesToTransform == null) { rule.keyframeNamesToTransform = []; for (let keyframe in keyframeTransforms) { transform = keyframeTransforms[keyframe]; output = transform(input); // If the transform actually changed the CSS text, we cache the // transform name for future use: if (input !== output) { input = output; rule.keyframeNamesToTransform.push(keyframe); } } } else { // If we already have a list of keyframe names that apply to this // rule, we apply only those keyframe name transforms: for (let i = 0; i < rule.keyframeNamesToTransform.length; ++i) { transform = keyframeTransforms[rule.keyframeNamesToTransform[i]]; input = transform(input); } output = input; } } rule['cssText'] = output; } // Test if the rules in these styles matches the given `element` and if so, // collect any custom properties into `props`. /** * @param {StyleNode} rules * @param {Element} element */ propertyDataFromStyles(rules, element) { let props = {}, self = this; // generates a unique key for these matches let o = []; // note: active rules excludes non-matching @media rules StyleUtil.forEachRule(rules, function(rule) { // TODO(sorvell): we could trim the set of rules at declaration // time to only include ones that have properties if (!rule.propertyInfo) { self.decorateRule(rule); } // match element against transformedSelector: selector may contain // unwanted uniquification and parsedSelector does not directly match // for :host selectors. let selectorToMatch = rule.transformedSelector || rule['parsedSelector']; if (element && rule.propertyInfo.properties && selectorToMatch) { if (matchesSelector.call(element, selectorToMatch)) { self.collectProperties(rule, props); // produce numeric key for these matches for lookup addToBitMask(rule.index, o); } } }, null, true); return {properties: props, key: o}; } /** * @param {Element} scope * @param {StyleNode} rule * @param {string|undefined} cssBuild * @param {function(Object)} callback */ whenHostOrRootRule(scope, rule, cssBuild, callback) { if (!rule.propertyInfo) { this.decorateRule(rule); } if (!rule.propertyInfo.properties) { return; } let {is, typeExtension} = StyleUtil.getIsExtends(scope); let hostScope = is ? StyleTransformer._calcHostScope(is, typeExtension) : 'html'; let parsedSelector = rule['parsedSelector']; let isRoot = (parsedSelector === ':host > *' || parsedSelector === 'html'); let isHost = parsedSelector.indexOf(':host') === 0 && !isRoot; // build info is either in scope (when scope is an element) or in the style // when scope is the default scope; note: this allows default scope to have // mixed mode built and unbuilt styles. if (cssBuild === 'shady') { // :root -> x-foo > *.x-foo for elements and html for custom-style isRoot = parsedSelector === (hostScope + ' > *.' + hostScope) || parsedSelector.indexOf('html') !== -1; // :host -> x-foo for elements, but sub-rules have .x-foo in them isHost = !isRoot && parsedSelector.indexOf(hostScope) === 0; } if (cssBuild === 'shadow') { isRoot = parsedSelector === ':host > *' || parsedSelector === 'html'; isHost = isHost && !isRoot; } if (!isRoot && !isHost) { return; } let selectorToMatch = hostScope; if (isHost) { // need to transform :host because `:host` does not work with `matches` if (!rule.transformedSelector) { // transform :host into a matchable selector rule.transformedSelector = StyleTransformer._transformRuleCss( rule, StyleTransformer._transformComplexSelector, StyleTransformer._calcElementScope(is), hostScope ); } selectorToMatch = rule.transformedSelector || hostScope; } callback({ selector: selectorToMatch, isHost: isHost, isRoot: isRoot }); } /** * @param {Element} scope * @param {StyleNode} rules * @return {Object} */ hostAndRootPropertiesForScope(scope, rules) { let hostProps = {}, rootProps = {}, self = this; // note: active rules excludes non-matching @media rules let cssBuild = rules && rules['__cssBuild']; StyleUtil.forEachRule(rules, function(rule) { // if scope is StyleDefaults, use _element for matchesSelector self.whenHostOrRootRule(scope, rule, cssBuild, function(info) { let element = scope._element || scope; if (matchesSelector.call(element, info.selector)) { if (info.isHost) { self.collectProperties(rule, hostProps); } else { self.collectProperties(rule, rootProps); } } }); }, null, true); return {rootProps: rootProps, hostProps: hostProps}; } /** * @param {Element} element * @param {Object} properties * @param {string} scopeSelector */ transformStyles(element, properties, scopeSelector) { let self = this; let {is, typeExtension} = StyleUtil.getIsExtends(element); let hostSelector = StyleTransformer ._calcHostScope(is, typeExtension); let rxHostSelector = element.extends ? '\\' + hostSelector.slice(0, -1) + '\\]' : hostSelector; let hostRx = new RegExp(RX.HOST_PREFIX + rxHostSelector + RX.HOST_SUFFIX); let rules = StyleInfo.get(element).styleRules; let keyframeTransforms = this._elementKeyframeTransforms(element, rules, scopeSelector); return StyleTransformer.elementStyles(element, rules, function(rule) { self.applyProperties(rule, properties); if (!nativeShadow && !StyleUtil.isKeyframesSelector(rule) && rule['cssText']) { // NOTE: keyframe transforms only scope munge animation names, so it // is not necessary to apply them in ShadowDOM. self.applyKeyframeTransforms(rule, keyframeTransforms); self._scopeSelector(rule, hostRx, hostSelector, scopeSelector); } }); } /** * @param {Element} element * @param {StyleNode} rules * @param {string} scopeSelector * @return {Object} */ _elementKeyframeTransforms(element, rules, scopeSelector) { let keyframesRules = rules._keyframes; let keyframeTransforms = {}; if (!nativeShadow && keyframesRules) { // For non-ShadowDOM, we transform all known keyframes rules in // advance for the current scope. This allows us to catch keyframes // rules that appear anywhere in the stylesheet: for (let i = 0, keyframesRule = keyframesRules[i]; i < keyframesRules.length; keyframesRule = keyframesRules[++i]) { this._scopeKeyframes(keyframesRule, scopeSelector); keyframeTransforms[keyframesRule['keyframesName']] = this._keyframesRuleTransformer(keyframesRule); } } return keyframeTransforms; } // Generate a factory for transforming a chunk of CSS text to handle a // particular scoped keyframes rule. /** * @param {StyleNode} keyframesRule * @return {function(string):string} */ _keyframesRuleTransformer(keyframesRule) { return function(cssText) { return cssText.replace( keyframesRule.keyframesNameRx, keyframesRule.transformedKeyframesName); }; } /** * Transforms `@keyframes` names to be unique for the current host. * Example: @keyframes foo-anim -> @keyframes foo-anim-x-foo-0 * * @param {StyleNode} rule * @param {string} scopeId */ _scopeKeyframes(rule, scopeId) { // Animation names are of the form [\w-], so ensure that the name regex does not partially apply // to similarly named keyframe names by checking for a word boundary at the beginning and // a non-word boundary or `-` at the end. rule.keyframesNameRx = new RegExp(`\\b${rule['keyframesName']}(?!\\B|-)`, 'g'); rule.transformedKeyframesName = rule['keyframesName'] + '-' + scopeId; rule.transformedSelector = rule.transformedSelector || rule['selector']; rule['selector'] = rule.transformedSelector.replace( rule['keyframesName'], rule.transformedKeyframesName); } // Strategy: x scope shim a selector e.g. to scope `.x-foo-42` (via classes): // non-host selector: .a.x-foo -> .x-foo-42 .a.x-foo // host selector: x-foo.wide -> .x-foo-42.wide // note: we use only the scope class (.x-foo-42) and not the hostSelector // (x-foo) to scope :host rules; this helps make property host rules // have low specificity. They are overrideable by class selectors but, // unfortunately, not by type selectors (e.g. overriding via // `.special` is ok, but not by `x-foo`). /** * @param {StyleNode} rule * @param {RegExp} hostRx * @param {string} hostSelector * @param {string} scopeId */ _scopeSelector(rule, hostRx, hostSelector, scopeId) { rule.transformedSelector = rule.transformedSelector || rule['selector']; let selector = rule.transformedSelector; let scope = '.' + scopeId; let parts = selector.split(','); for (let i=0, l=parts.length, p; (i<l) && (p=parts[i]); i++) { parts[i] = p.match(hostRx) ? p.replace(hostSelector, scope) : scope + ' ' + p; } rule['selector'] = parts.join(','); } /** * @param {Element} element * @param {string} selector * @param {string} old */ applyElementScopeSelector(element, selector, old) { let c = element.getAttribute('class') || ''; let v = c; if (old) { v = c.replace( new RegExp('\\s*' + XSCOPE_NAME + '\\s*' + old + '\\s*', 'g'), ' '); } v += (v ? ' ' : '') + XSCOPE_NAME + ' ' + selector; if (c !== v) { StyleUtil.setElementClassRaw(element, v); } } /** * @param {HTMLElement} element * @param {Object} properties * @param {string} selector * @param {HTMLStyleElement} style * @return {HTMLStyleElement} */ applyElementStyle(element, properties, selector, style) { // calculate cssText to apply let cssText = style ? style.textContent || '' : this.transformStyles(element, properties, selector); // if shady and we have a cached style that is not style, decrement let styleInfo = StyleInfo.get(element); let s = styleInfo.customStyle; if (s && !nativeShadow && (s !== style)) { s['_useCount']--; if (s['_useCount'] <= 0 && s.parentNode) { s.parentNode.removeChild(s); } } // apply styling always under native or if we generated style // or the cached style is not in document(!) if (nativeShadow) { // update existing style only under native if (styleInfo.customStyle) { styleInfo.customStyle.textContent = cssText; style = styleInfo.customStyle; // otherwise, if we have css to apply, do so } else if (cssText) { // apply css after the scope style of the element to help with // style precedence rules. style = StyleUtil.applyCss(cssText, selector, element.shadowRoot, styleInfo.placeholder); } } else { // shady and no cache hit if (!style) { // apply css after the scope style of the element to help with // style precedence rules. if (cssText) { style = StyleUtil.applyCss(cssText, selector, null, styleInfo.placeholder); } // shady and cache hit but not in document } else if (!style.parentNode) { if (IS_IE && cssText.indexOf('@media') > -1) { // @media rules may be stale in IE 10 and 11 // refresh the text content of the style to revalidate them. style.textContent = cssText; } StyleUtil.applyStyle(style, null, styleInfo.placeholder); } } // ensure this style is our custom style and increment its use count. if (style) { style['_useCount'] = style['_useCount'] || 0; // increment use count if we changed styles if (styleInfo.customStyle != style) { style['_useCount']++; } styleInfo.customStyle = style; } return style; } /** * @param {Element} style * @param {Object} properties */ applyCustomStyle(style, properties) { let rules = StyleUtil.rulesForStyle(/** @type {HTMLStyleElement} */(style)); let self = this; style.textContent = StyleUtil.toCssText(rules, function(/** StyleNode */rule) { let css = rule['cssText'] = rule['parsedCssText']; if (rule.propertyInfo && rule.propertyInfo.cssText) { // remove property assignments // so next function isn't confused // NOTE: we have 3 categories of css: // (1) normal properties, // (2) custom property assignments (--foo: red;), // (3) custom property usage: border: var(--foo); @apply(--foo); // In elements, 1 and 3 are separated for efficiency; here they // are not and this makes this case unique. css = removeCustomPropAssignment(/** @type {string} */(css)); // replace with reified properties, scenario is same as mixin rule['cssText'] = self.valueForProperties(css, properties); } }); } } /** * @param {number} n * @param {Array<number>} bits */ function addToBitMask(n, bits) { let o = parseInt(n / 32, 10); let v = 1 << (n % 32); bits[o] = (bits[o] || 0) | v; } export default new StyleProperties();