aboutsummaryrefslogtreecommitdiff
path: root/catapult/third_party/polymer/components/shadycss/src/style-transformer.js
diff options
context:
space:
mode:
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.js487
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()