diff options
Diffstat (limited to 'catapult/third_party/polymer/components/shadycss/src/style-transformer.js')
-rw-r--r-- | catapult/third_party/polymer/components/shadycss/src/style-transformer.js | 487 |
1 files changed, 487 insertions, 0 deletions
diff --git a/catapult/third_party/polymer/components/shadycss/src/style-transformer.js b/catapult/third_party/polymer/components/shadycss/src/style-transformer.js new file mode 100644 index 00000000..a4623a6b --- /dev/null +++ b/catapult/third_party/polymer/components/shadycss/src/style-transformer.js @@ -0,0 +1,487 @@ +/** +@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 {StyleNode} from './css-parse.js'; // eslint-disable-line no-unused-vars +import * as StyleUtil from './style-util.js'; +import {nativeShadow} from './style-settings.js'; + +/* Transforms ShadowDOM styling into ShadyDOM styling + +* scoping: + + * elements in scope get scoping selector class="x-foo-scope" + * selectors re-written as follows: + + div button -> div.x-foo-scope button.x-foo-scope + +* :host -> scopeName + +* :host(...) -> scopeName... + +* ::slotted(...) -> scopeName > ... + +* ...:dir(ltr|rtl) -> [dir="ltr|rtl"] ..., ...[dir="ltr|rtl"] + +* :host(:dir[rtl]) -> scopeName:dir(rtl) -> [dir="rtl"] scopeName, scopeName[dir="rtl"] + +*/ +const SCOPE_NAME = 'style-scope'; + +class StyleTransformer { + get SCOPE_NAME() { + return SCOPE_NAME; + } + /** + * Given a node and scope name, add a scoping class to each node + * in the tree. This facilitates transforming css into scoped rules. + * @param {!Node} node + * @param {string} scope + * @param {boolean=} shouldRemoveScope + * @deprecated + */ + dom(node, scope, shouldRemoveScope) { + const fn = (node) => { + this.element(node, scope || '', shouldRemoveScope); + }; + this._transformDom(node, fn); + } + + /** + * Given a node and scope name, add a scoping class to each node in the tree. + * @param {!Node} node + * @param {string} scope + */ + domAddScope(node, scope) { + const fn = (node) => { + this.element(node, scope || ''); + }; + this._transformDom(node, fn); + } + + /** + * @param {!Node} startNode + * @param {!function(!Node)} transformer + */ + _transformDom(startNode, transformer) { + if (startNode.nodeType === Node.ELEMENT_NODE) { + transformer(startNode) + } + let c$; + if (startNode.localName === 'template') { + const template = /** @type {!HTMLTemplateElement} */ (startNode); + // In case the template is in svg context, fall back to the node + // since it won't be an HTMLTemplateElement with a .content property + c$ = (template.content || template._content || template).childNodes; + } else { + c$ = /** @type {!ParentNode} */ (startNode).children || + startNode.childNodes; + } + if (c$) { + for (let i = 0; i < c$.length; i++) { + this._transformDom(c$[i], transformer); + } + } + } + + /** + * @param {?} element + * @param {?} scope + * @param {?=} shouldRemoveScope + */ + element(element, scope, shouldRemoveScope) { + // note: if using classes, we add both the general 'style-scope' class + // as well as the specific scope. This enables easy filtering of all + // `style-scope` elements + if (scope) { + // note: svg on IE does not have classList so fallback to class + if (element.classList) { + if (shouldRemoveScope) { + element.classList.remove(SCOPE_NAME); + element.classList.remove(scope); + } else { + element.classList.add(SCOPE_NAME); + element.classList.add(scope); + } + } else if (element.getAttribute) { + let c = element.getAttribute(CLASS); + if (shouldRemoveScope) { + if (c) { + let newValue = c.replace(SCOPE_NAME, '').replace(scope, ''); + StyleUtil.setElementClassRaw(element, newValue); + } + } else { + let newValue = (c ? c + ' ' : '') + SCOPE_NAME + ' ' + scope; + StyleUtil.setElementClassRaw(element, newValue); + } + } + } + } + + /** + * Given a node, replace the scoping class to each subnode in the tree. + * @param {!Node} node + * @param {string} oldScope + * @param {string} newScope + */ + domReplaceScope(node, oldScope, newScope) { + const fn = (node) => { + this.element(node, oldScope, true); + this.element(node, newScope); + }; + this._transformDom(node, fn); + } + /** + * Given a node, remove the scoping class to each subnode in the tree. + * @param {!Node} node + * @param {string} oldScope + */ + domRemoveScope(node, oldScope) { + const fn = (node) => { + this.element(node, oldScope || '', true); + }; + this._transformDom(node, fn); + } + + /** + * @param {?} element + * @param {?} styleRules + * @param {?=} callback + * @param {string=} cssBuild + * @param {string=} cssText + * @return {string} + */ + elementStyles(element, styleRules, callback, cssBuild = '', cssText = '') { + // no need to shim selectors if settings.useNativeShadow, also + // a shady css build will already have transformed selectors + // NOTE: This method may be called as part of static or property shimming. + // When there is a targeted build it will not be called for static shimming, + // but when the property shim is used it is called and should opt out of + // static shimming work when a proper build exists. + if (cssText === '') { + if (nativeShadow || cssBuild === 'shady') { + cssText = StyleUtil.toCssText(styleRules, callback); + } else { + let {is, typeExtension} = StyleUtil.getIsExtends(element); + cssText = this.css(styleRules, is, typeExtension, callback) + '\n\n'; + } + } + return cssText.trim(); + } + + // Given a string of cssText and a scoping string (scope), returns + // a string of scoped css where each selector is transformed to include + // a class created from the scope. ShadowDOM selectors are also transformed + // (e.g. :host) to use the scoping selector. + css(rules, scope, ext, callback) { + let hostScope = this._calcHostScope(scope, ext); + scope = this._calcElementScope(scope); + let self = this; + return StyleUtil.toCssText(rules, function(/** StyleNode */rule) { + if (!rule.isScoped) { + self.rule(rule, scope, hostScope); + rule.isScoped = true; + } + if (callback) { + callback(rule, scope, hostScope); + } + }); + } + + _calcElementScope(scope) { + if (scope) { + return CSS_CLASS_PREFIX + scope; + } else { + return ''; + } + } + + _calcHostScope(scope, ext) { + return ext ? `[is=${scope}]` : scope; + } + + rule(rule, scope, hostScope) { + this._transformRule(rule, this._transformComplexSelector, + scope, hostScope); + } + + /** + * transforms a css rule to a scoped rule. + * + * @param {StyleNode} rule + * @param {Function} transformer + * @param {string=} scope + * @param {string=} hostScope + */ + _transformRule(rule, transformer, scope, hostScope) { + // NOTE: save transformedSelector for subsequent matching of elements + // against selectors (e.g. when calculating style properties) + rule['selector'] = rule.transformedSelector = + this._transformRuleCss(rule, transformer, scope, hostScope); + } + + /** + * @param {StyleNode} rule + * @param {Function} transformer + * @param {string=} scope + * @param {string=} hostScope + */ + _transformRuleCss(rule, transformer, scope, hostScope) { + let p$ = StyleUtil.splitSelectorList(rule['selector']); + // we want to skip transformation of rules that appear in keyframes, + // because they are keyframe selectors, not element selectors. + if (!StyleUtil.isKeyframesSelector(rule)) { + for (let i=0, l=p$.length, p; (i<l) && (p=p$[i]); i++) { + p$[i] = transformer.call(this, p, scope, hostScope); + } + } + return p$.filter((part) => Boolean(part)).join(COMPLEX_SELECTOR_SEP); + } + + /** + * @param {string} selector + * @return {string} + */ + _twiddleNthPlus(selector) { + return selector.replace(NTH, (m, type, inside) => { + if (inside.indexOf('+') > -1) { + inside = inside.replace(/\+/g, '___'); + } else if (inside.indexOf('___') > -1) { + inside = inside.replace(/___/g, '+'); + } + return `:${type}(${inside})`; + }); + } + + /** + * Preserve `:matches()` selectors by replacing them with MATCHES_REPLACMENT + * and returning an array of `:matches()` selectors. + * Use `_replacesMatchesPseudo` to replace the `:matches()` parts + * + * @param {string} selector + * @return {{selector: string, matches: !Array<string>}} + */ + _preserveMatchesPseudo(selector) { + /** @type {!Array<string>} */ + const matches = []; + let match; + while ((match = selector.match(MATCHES))) { + const start = match.index; + const end = StyleUtil.findMatchingParen(selector, start); + if (end === -1) { + throw new Error(`${match.input} selector missing ')'`) + } + const part = selector.slice(start, end + 1); + selector = selector.replace(part, MATCHES_REPLACEMENT); + matches.push(part); + } + return {selector, matches}; + } + + /** + * Replace MATCHES_REPLACMENT character with the given set of `:matches()` + * selectors. + * + * @param {string} selector + * @param {!Array<string>} matches + * @return {string} + */ + _replaceMatchesPseudo(selector, matches) { + const parts = selector.split(MATCHES_REPLACEMENT); + return matches.reduce((acc, cur, idx) => acc + cur + parts[idx + 1], parts[0]); + } + +/** + * @param {string} selector + * @param {string} scope + * @param {string=} hostScope + */ + _transformComplexSelector(selector, scope, hostScope) { + let stop = false; + selector = selector.trim(); + // Remove spaces inside of selectors like `:nth-of-type` because it confuses SIMPLE_SELECTOR_SEP + let isNth = NTH.test(selector); + if (isNth) { + selector = selector.replace(NTH, (m, type, inner) => `:${type}(${inner.replace(/\s/g, '')})`) + selector = this._twiddleNthPlus(selector); + } + // Preserve selectors like `:-webkit-any` so that SIMPLE_SELECTOR_SEP does + // not get confused by spaces inside the pseudo selector + const isMatches = MATCHES.test(selector); + /** @type {!Array<string>} */ + let matches; + if (isMatches) { + ({selector, matches} = this._preserveMatchesPseudo(selector)); + } + selector = selector.replace(SLOTTED_START, `${HOST} $1`); + selector = selector.replace(SIMPLE_SELECTOR_SEP, (m, c, s) => { + if (!stop) { + let info = this._transformCompoundSelector(s, c, scope, hostScope); + stop = stop || info.stop; + c = info.combinator; + s = info.value; + } + return c + s; + }); + // replace `:matches()` selectors + if (isMatches) { + selector = this._replaceMatchesPseudo(selector, matches); + } + if (isNth) { + selector = this._twiddleNthPlus(selector); + } + return selector; + } + + _transformCompoundSelector(selector, combinator, scope, hostScope) { + // replace :host with host scoping class + let slottedIndex = selector.indexOf(SLOTTED); + if (selector.indexOf(HOST) >= 0) { + selector = this._transformHostSelector(selector, hostScope); + // replace other selectors with scoping class + } else if (slottedIndex !== 0) { + selector = scope ? this._transformSimpleSelector(selector, scope) : + selector; + } + // mark ::slotted() scope jump to replace with descendant selector + arg + // also ignore left-side combinator + let slotted = false; + if (slottedIndex >= 0) { + combinator = ''; + slotted = true; + } + // process scope jumping selectors up to the scope jump and then stop + let stop; + if (slotted) { + stop = true; + if (slotted) { + // .zonk ::slotted(.foo) -> .zonk.scope > .foo + selector = selector.replace(SLOTTED_PAREN, (m, paren) => ` > ${paren}`); + } + } + selector = selector.replace(DIR_PAREN, (m, before, dir) => + `[dir="${dir}"] ${before}, ${before}[dir="${dir}"]`); + return {value: selector, combinator, stop}; + } + + _transformSimpleSelector(selector, scope) { + const attributes = selector.split(/(\[.+?\])/); + + const output = []; + for (let i = 0; i < attributes.length; i++) { + // Do not attempt to transform any attribute selector content + if ((i % 2) === 1) { + output.push(attributes[i]); + } else { + const part = attributes[i]; + + if (!(part === '' && i === attributes.length - 1)) { + let p$ = part.split(PSEUDO_PREFIX); + p$[0] += scope; + output.push(p$.join(PSEUDO_PREFIX)); + } + } + } + + return output.join(''); + } + + // :host(...) -> scopeName... + _transformHostSelector(selector, hostScope) { + let m = selector.match(HOST_PAREN); + let paren = m && m[2].trim() || ''; + if (paren) { + if (!paren[0].match(SIMPLE_SELECTOR_PREFIX)) { + // paren starts with a type selector + let typeSelector = paren.split(SIMPLE_SELECTOR_PREFIX)[0]; + // if the type selector is our hostScope then avoid pre-pending it + if (typeSelector === hostScope) { + return paren; + // otherwise, this selector should not match in this scope so + // output a bogus selector. + } else { + return SELECTOR_NO_MATCH; + } + } else { + // make sure to do a replace here to catch selectors like: + // `:host(.foo)::before` + return selector.replace(HOST_PAREN, function(m, host, paren) { + return hostScope + paren; + }); + } + // if no paren, do a straight :host replacement. + // TODO(sorvell): this should not strictly be necessary but + // it's needed to maintain support for `:host[foo]` type selectors + // which have been improperly used under Shady DOM. This should be + // deprecated. + } else { + return selector.replace(HOST, hostScope); + } + } + + /** + * @param {StyleNode} rule + */ + documentRule(rule) { + // reset selector in case this is redone. + rule['selector'] = rule['parsedSelector']; + this.normalizeRootSelector(rule); + this._transformRule(rule, this._transformDocumentSelector); + } + + /** + * @param {StyleNode} rule + */ + normalizeRootSelector(rule) { + if (rule['selector'] === ROOT) { + rule['selector'] = 'html'; + } + } + +/** + * @param {string} selector + */ + _transformDocumentSelector(selector) { + if (selector.match(HOST)) { + // remove ':host' type selectors in document rules + return ''; + } else if (selector.match(SLOTTED)) { + return this._transformComplexSelector(selector, SCOPE_DOC_SELECTOR) + } else { + return this._transformSimpleSelector(selector.trim(), SCOPE_DOC_SELECTOR); + } + } +} + +const NTH = /:(nth[-\w]+)\(([^)]+)\)/; +const SCOPE_DOC_SELECTOR = `:not(.${SCOPE_NAME})`; +const COMPLEX_SELECTOR_SEP = ','; +const SIMPLE_SELECTOR_SEP = /(^|[\s>+~]+)((?:\[.+?\]|[^\s>+~=[])+)/g; +const SIMPLE_SELECTOR_PREFIX = /[[.:#*]/; +const HOST = ':host'; +const ROOT = ':root'; +const SLOTTED = '::slotted'; +const SLOTTED_START = new RegExp(`^(${SLOTTED})`); +// NOTE: this supports 1 nested () pair for things like +// :host(:not([selected]), more general support requires +// parsing which seems like overkill +const HOST_PAREN = /(:host)(?:\(((?:\([^)(]*\)|[^)(]*)+?)\))/; +// similar to HOST_PAREN +const SLOTTED_PAREN = /(?:::slotted)(?:\(((?:\([^)(]*\)|[^)(]*)+?)\))/; +const DIR_PAREN = /(.*):dir\((?:(ltr|rtl))\)/; +const CSS_CLASS_PREFIX = '.'; +const PSEUDO_PREFIX = ':'; +const CLASS = 'class'; +const SELECTOR_NO_MATCH = 'should_not_match'; +const MATCHES = /:(?:matches|any|-(?:webkit|moz)-any)/; +const MATCHES_REPLACEMENT = '\u{e000}'; + +export default new StyleTransformer() |