UNPKG

@paraboly/pwc-multi-filter

Version:

A wrapper over pwc-tabview and pwc-filter. Provides means of dynamically managing multiple filters via a single component.

659 lines (658 loc) 21.9 kB
/* Extremely simple css parser. Intended to be not more than what we need and definitely not necessarily correct =). */ /** @unrestricted */ var StyleNode = /** @class */ (function () { function StyleNode() { this.start = 0; this.end = 0; this.previous = null; this.parent = null; this.rules = null; this.parsedCssText = ''; this.cssText = ''; this.atRule = false; this.type = 0; this.keyframesName = ''; this.selector = ''; this.parsedSelector = ''; } return StyleNode; }()); // given a string of css, return a simple rule tree /** * @param {string} text * @return {StyleNode} */ function parse(text) { text = clean(text); return parseCss(lex(text), text); } // remove stuff we don't care about that may hinder parsing /** * @param {string} cssText * @return {string} */ function clean(cssText) { return cssText.replace(RX.comments, '').replace(RX.port, ''); } // super simple {...} lexer that returns a node tree /** * @param {string} text * @return {StyleNode} */ function lex(text) { var root = new StyleNode(); root['start'] = 0; root['end'] = text.length; var n = root; for (var i = 0, l = text.length; i < l; i++) { if (text[i] === OPEN_BRACE) { if (!n['rules']) { n['rules'] = []; } var p = n; var previous = p['rules'][p['rules'].length - 1] || null; n = new StyleNode(); n['start'] = i + 1; n['parent'] = p; n['previous'] = previous; p['rules'].push(n); } else if (text[i] === CLOSE_BRACE) { n['end'] = i + 1; n = n['parent'] || root; } } return root; } // add selectors/cssText to node tree /** * @param {StyleNode} node * @param {string} text * @return {StyleNode} */ function parseCss(node, text) { var t = text.substring(node['start'], node['end'] - 1); node['parsedCssText'] = node['cssText'] = t.trim(); if (node.parent) { var ss = node.previous ? node.previous['end'] : node.parent['start']; t = text.substring(ss, node['start'] - 1); t = _expandUnicodeEscapes(t); t = t.replace(RX.multipleSpaces, ' '); // TODO(sorvell): ad hoc; make selector include only after last ; // helps with mixin syntax t = t.substring(t.lastIndexOf(';') + 1); var s = node['parsedSelector'] = node['selector'] = t.trim(); node['atRule'] = (s.indexOf(AT_START) === 0); // note, support a subset of rule types... if (node['atRule']) { if (s.indexOf(MEDIA_START) === 0) { node['type'] = types.MEDIA_RULE; } else if (s.match(RX.keyframesRule)) { node['type'] = types.KEYFRAMES_RULE; node['keyframesName'] = node['selector'].split(RX.multipleSpaces).pop(); } } else { if (s.indexOf(VAR_START) === 0) { node['type'] = types.MIXIN_RULE; } else { node['type'] = types.STYLE_RULE; } } } var r$ = node['rules']; if (r$) { for (var i = 0, l = r$.length, r = void 0; (i < l) && (r = r$[i]); i++) { parseCss(r, text); } } return node; } /** * conversion of sort unicode escapes with spaces like `\33 ` (and longer) into * expanded form that doesn't require trailing space `\000033` * @param {string} s * @return {string} */ function _expandUnicodeEscapes(s) { return s.replace(/\\([0-9a-f]{1,6})\s/gi, function () { var code = arguments[1], repeat = 6 - code.length; while (repeat--) { code = '0' + code; } return '\\' + code; }); } /** @enum {number} */ var types = { STYLE_RULE: 1, KEYFRAMES_RULE: 7, MEDIA_RULE: 4, MIXIN_RULE: 1000 }; var OPEN_BRACE = '{'; var CLOSE_BRACE = '}'; // helper regexp's var RX = { comments: /\/\*[^*]*\*+([^/*][^*]*\*+)*\//gim, port: /@import[^;]*;/gim, customProp: /(?:^[^;\-\s}]+)?--[^;{}]*?:[^{};]*?(?:[;\n]|$)/gim, mixinProp: /(?:^[^;\-\s}]+)?--[^;{}]*?:[^{};]*?{[^}]*?}(?:[;\n]|$)?/gim, mixinApply: /@apply\s*\(?[^);]*\)?\s*(?:[;\n]|$)?/gim, varApply: /[^;:]*?:[^;]*?var\([^;]*\)(?:[;\n]|$)?/gim, keyframesRule: /^@[^\s]*keyframes/, multipleSpaces: /\s+/g }; var VAR_START = '--'; var MEDIA_START = '@media'; var AT_START = '@'; function findRegex(regex, cssText, offset) { regex['lastIndex'] = 0; var r = cssText.substring(offset).match(regex); if (r) { var start = offset + r['index']; return { start: start, end: start + r[0].length }; } return null; } var VAR_USAGE_START = /\bvar\(/; var VAR_ASSIGN_START = /\B--[\w-]+\s*:/; var COMMENTS = /\/\*[^*]*\*+([^/*][^*]*\*+)*\//gim; var TRAILING_LINES = /^[\t ]+\n/gm; function resolveVar(props, prop, fallback) { if (props[prop]) { return props[prop]; } if (fallback) { return executeTemplate(fallback, props); } return ''; } function findVarEndIndex(cssText, offset) { var count = 0; var i = offset; for (; i < cssText.length; i++) { var c = cssText[i]; if (c === '(') { count++; } else if (c === ')') { count--; if (count <= 0) { return i + 1; } } } return i; } function parseVar(cssText, offset) { var varPos = findRegex(VAR_USAGE_START, cssText, offset); if (!varPos) { return null; } var endVar = findVarEndIndex(cssText, varPos.start); var varContent = cssText.substring(varPos.end, endVar - 1); var _a = varContent.split(','), propName = _a[0], fallback = _a.slice(1); return { start: varPos.start, end: endVar, propName: propName.trim(), fallback: fallback.length > 0 ? fallback.join(',').trim() : undefined }; } function compileVar(cssText, template, offset) { var varMeta = parseVar(cssText, offset); if (!varMeta) { template.push(cssText.substring(offset, cssText.length)); return cssText.length; } var propName = varMeta.propName; var fallback = varMeta.fallback != null ? compileTemplate(varMeta.fallback) : undefined; template.push(cssText.substring(offset, varMeta.start), function (params) { return resolveVar(params, propName, fallback); }); return varMeta.end; } function executeTemplate(template, props) { var final = ''; for (var i = 0; i < template.length; i++) { var s = template[i]; final += (typeof s === 'string') ? s : s(props); } return final; } function findEndValue(cssText, offset) { var onStr = false; var double = false; var i = offset; for (; i < cssText.length; i++) { var c = cssText[i]; if (onStr) { if (double && c === '"') { onStr = false; } if (!double && c === '\'') { onStr = false; } } else { if (c === '"') { onStr = true; double = true; } else if (c === '\'') { onStr = true; double = false; } else if (c === ';') { return i + 1; } else if (c === '}') { return i; } } } return i; } function removeCustomAssigns(cssText) { var final = ''; var offset = 0; while (true) { var assignPos = findRegex(VAR_ASSIGN_START, cssText, offset); var start = assignPos ? assignPos.start : cssText.length; final += cssText.substring(offset, start); if (assignPos) { offset = findEndValue(cssText, start); } else { break; } } return final; } function compileTemplate(cssText) { var index = 0; cssText = cssText.replace(COMMENTS, ''); cssText = removeCustomAssigns(cssText) .replace(TRAILING_LINES, ''); var segments = []; while (index < cssText.length) { index = compileVar(cssText, segments, index); } return segments; } function resolveValues(selectors) { var props = {}; selectors.forEach(function (selector) { selector.declarations.forEach(function (dec) { props[dec.prop] = dec.value; }); }); var propsValues = {}; var entries = Object.entries(props); var _loop_1 = function (i) { var dirty = false; entries.forEach(function (_a) { var key = _a[0], value = _a[1]; var propValue = executeTemplate(value, propsValues); if (propValue !== propsValues[key]) { propsValues[key] = propValue; dirty = true; } }); if (!dirty) { return "break"; } }; for (var i = 0; i < 10; i++) { var state_1 = _loop_1(); if (state_1 === "break") break; } return propsValues; } function getSelectors(root, index) { if (index === void 0) { index = 0; } if (!root.rules) { return []; } var selectors = []; root.rules .filter(function (rule) { return rule.type === types.STYLE_RULE; }) .forEach(function (rule) { var declarations = getDeclarations(rule.cssText); if (declarations.length > 0) { rule.parsedSelector.split(',').forEach(function (selector) { selector = selector.trim(); selectors.push({ selector: selector, declarations: declarations, specificity: computeSpecificity(), nu: index }); }); } index++; }); return selectors; } function computeSpecificity(_selector) { return 1; } var IMPORTANT = '!important'; var FIND_DECLARATIONS = /(?:^|[;\s{]\s*)(--[\w-]*?)\s*:\s*(?:((?:'(?:\\'|.)*?'|"(?:\\"|.)*?"|\([^)]*?\)|[^};{])+)|\{([^}]*)\}(?:(?=[;\s}])|$))/gm; function getDeclarations(cssText) { var declarations = []; var xArray; while (xArray = FIND_DECLARATIONS.exec(cssText.trim())) { var _a = normalizeValue(xArray[2]), value = _a.value, important = _a.important; declarations.push({ prop: xArray[1].trim(), value: compileTemplate(value), important: important, }); } return declarations; } function normalizeValue(value) { var regex = /\s+/gim; value = value.replace(regex, ' ').trim(); var important = value.endsWith(IMPORTANT); if (important) { value = value.substr(0, value.length - IMPORTANT.length).trim(); } return { value: value, important: important }; } function getActiveSelectors(hostEl, hostScopeMap, globalScopes) { // computes the css scopes that might affect this particular element // avoiding using spread arrays to avoid ts helper fns when in es5 var scopes = []; var scopesForElement = getScopesForElement(hostScopeMap, hostEl); // globalScopes are always took into account globalScopes.forEach(function (s) { return scopes.push(s); }); // the parent scopes are computed by walking parent dom until <html> is reached scopesForElement.forEach(function (s) { return scopes.push(s); }); // each scope might have an array of associated selectors // let's flatten the complete array of selectors from all the scopes var selectorSet = getSelectorsForScopes(scopes); // we filter to only the selectors that matches the hostEl var activeSelectors = selectorSet.filter(function (selector) { return matches(hostEl, selector.selector); }); // sort selectors by specifity return sortSelectors(activeSelectors); } function getScopesForElement(hostTemplateMap, node) { var scopes = []; while (node) { var scope = hostTemplateMap.get(node); if (scope) { scopes.push(scope); } node = node.parentElement; } return scopes; } function getSelectorsForScopes(scopes) { var selectors = []; scopes.forEach(function (scope) { selectors.push.apply(selectors, scope.selectors); }); return selectors; } function sortSelectors(selectors) { selectors.sort(function (a, b) { if (a.specificity === b.specificity) { return a.nu - b.nu; } return a.specificity - b.specificity; }); return selectors; } function matches(el, selector) { return selector === ':root' || selector === 'html' || el.matches(selector); } function parseCSS(original) { var ast = parse(original); var template = compileTemplate(original); var selectors = getSelectors(ast); return { original: original, template: template, selectors: selectors, usesCssVars: template.length > 1 }; } function addGlobalStyle(globalScopes, styleEl) { if (globalScopes.some(function (css) { return css.styleEl === styleEl; })) { return false; } var css = parseCSS(styleEl.textContent); css.styleEl = styleEl; globalScopes.push(css); return true; } function updateGlobalScopes(scopes) { var selectors = getSelectorsForScopes(scopes); var props = resolveValues(selectors); scopes.forEach(function (scope) { if (scope.usesCssVars) { scope.styleEl.textContent = executeTemplate(scope.template, props); } }); } function reScope(scope, scopeId) { var template = scope.template.map(function (segment) { return (typeof segment === 'string') ? replaceScope(segment, scope.scopeId, scopeId) : segment; }); var selectors = scope.selectors.map(function (sel) { return Object.assign(Object.assign({}, sel), { selector: replaceScope(sel.selector, scope.scopeId, scopeId) }); }); return Object.assign(Object.assign({}, scope), { template: template, selectors: selectors, scopeId: scopeId }); } function replaceScope(original, oldScopeId, newScopeId) { original = replaceAll(original, "\\." + oldScopeId, "." + newScopeId); return original; } function replaceAll(input, find, replace) { return input.replace(new RegExp(find, 'g'), replace); } function loadDocument(doc, globalScopes) { loadDocumentStyles(doc, globalScopes); return loadDocumentLinks(doc, globalScopes).then(function () { updateGlobalScopes(globalScopes); }); } function startWatcher(doc, globalScopes) { var mutation = new MutationObserver(function () { if (loadDocumentStyles(doc, globalScopes)) { updateGlobalScopes(globalScopes); } }); mutation.observe(document.head, { childList: true }); } function loadDocumentLinks(doc, globalScopes) { var promises = []; var linkElms = doc.querySelectorAll('link[rel="stylesheet"][href]:not([data-no-shim])'); for (var i = 0; i < linkElms.length; i++) { promises.push(addGlobalLink(doc, globalScopes, linkElms[i])); } return Promise.all(promises); } function loadDocumentStyles(doc, globalScopes) { var styleElms = Array.from(doc.querySelectorAll('style:not([data-styles]):not([data-no-shim])')); return styleElms .map(function (style) { return addGlobalStyle(globalScopes, style); }) .some(Boolean); } function addGlobalLink(doc, globalScopes, linkElm) { var url = linkElm.href; return fetch(url).then(function (rsp) { return rsp.text(); }).then(function (text) { if (hasCssVariables(text) && linkElm.parentNode) { if (hasRelativeUrls(text)) { text = fixRelativeUrls(text, url); } var styleEl = doc.createElement('style'); styleEl.setAttribute('data-styles', ''); styleEl.textContent = text; addGlobalStyle(globalScopes, styleEl); linkElm.parentNode.insertBefore(styleEl, linkElm); linkElm.remove(); } }).catch(function (err) { console.error(err); }); } // This regexp tries to determine when a variable is declared, for example: // // .my-el { --highlight-color: green; } // // but we don't want to trigger when a classname uses "--" or a pseudo-class is // used. We assume that the only characters that can preceed a variable // declaration are "{", from an opening block, ";" from a preceeding rule, or a // space. This prevents the regexp from matching a word in a selector, since // they would need to start with a "." or "#". (We assume element names don't // start with "--"). var CSS_VARIABLE_REGEXP = /[\s;{]--[-a-zA-Z0-9]+\s*:/m; function hasCssVariables(css) { return css.indexOf('var(') > -1 || CSS_VARIABLE_REGEXP.test(css); } // This regexp find all url() usages with relative urls var CSS_URL_REGEXP = /url[\s]*\([\s]*['"]?(?!(?:https?|data)\:|\/)([^\'\"\)]*)[\s]*['"]?\)[\s]*/gim; function hasRelativeUrls(css) { CSS_URL_REGEXP.lastIndex = 0; return CSS_URL_REGEXP.test(css); } function fixRelativeUrls(css, originalUrl) { // get the basepath from the original import url var basePath = originalUrl.replace(/[^/]*$/, ''); // replace the relative url, with the new relative url return css.replace(CSS_URL_REGEXP, function (fullMatch, url) { // rhe new relative path is the base path + uri // TODO: normalize relative URL var relativeUrl = basePath + url; return fullMatch.replace(url, relativeUrl); }); } var CustomStyle = /** @class */ (function () { function CustomStyle(win, doc) { this.win = win; this.doc = doc; this.count = 0; this.hostStyleMap = new WeakMap(); this.hostScopeMap = new WeakMap(); this.globalScopes = []; this.scopesMap = new Map(); this.didInit = false; } CustomStyle.prototype.initShim = function () { var _this = this; if (this.didInit) { return Promise.resolve(); } else { this.didInit = true; return new Promise(function (resolve) { _this.win.requestAnimationFrame(function () { startWatcher(_this.doc, _this.globalScopes); loadDocument(_this.doc, _this.globalScopes).then(function () { return resolve(); }); }); }); } }; CustomStyle.prototype.addLink = function (linkEl) { var _this = this; return addGlobalLink(this.doc, this.globalScopes, linkEl).then(function () { _this.updateGlobal(); }); }; CustomStyle.prototype.addGlobalStyle = function (styleEl) { if (addGlobalStyle(this.globalScopes, styleEl)) { this.updateGlobal(); } }; CustomStyle.prototype.createHostStyle = function (hostEl, cssScopeId, cssText, isScoped) { if (this.hostScopeMap.has(hostEl)) { throw new Error('host style already created'); } var baseScope = this.registerHostTemplate(cssText, cssScopeId, isScoped); var styleEl = this.doc.createElement('style'); styleEl.setAttribute('data-no-shim', ''); if (!baseScope.usesCssVars) { // This component does not use (read) css variables styleEl.textContent = cssText; } else if (isScoped) { // This component is dynamic: uses css var and is scoped styleEl['s-sc'] = cssScopeId = baseScope.scopeId + "-" + this.count; styleEl.textContent = '/*needs update*/'; this.hostStyleMap.set(hostEl, styleEl); this.hostScopeMap.set(hostEl, reScope(baseScope, cssScopeId)); this.count++; } else { // This component uses css vars, but it's no-encapsulation (global static) baseScope.styleEl = styleEl; if (!baseScope.usesCssVars) { styleEl.textContent = executeTemplate(baseScope.template, {}); } this.globalScopes.push(baseScope); this.updateGlobal(); this.hostScopeMap.set(hostEl, baseScope); } return styleEl; }; CustomStyle.prototype.removeHost = function (hostEl) { var css = this.hostStyleMap.get(hostEl); if (css) { css.remove(); } this.hostStyleMap.delete(hostEl); this.hostScopeMap.delete(hostEl); }; CustomStyle.prototype.updateHost = function (hostEl) { var scope = this.hostScopeMap.get(hostEl); if (scope && scope.usesCssVars && scope.isScoped) { var styleEl = this.hostStyleMap.get(hostEl); if (styleEl) { var selectors = getActiveSelectors(hostEl, this.hostScopeMap, this.globalScopes); var props = resolveValues(selectors); styleEl.textContent = executeTemplate(scope.template, props); } } }; CustomStyle.prototype.updateGlobal = function () { updateGlobalScopes(this.globalScopes); }; CustomStyle.prototype.registerHostTemplate = function (cssText, scopeId, isScoped) { var scope = this.scopesMap.get(scopeId); if (!scope) { scope = parseCSS(cssText); scope.scopeId = scopeId; scope.isScoped = isScoped; this.scopesMap.set(scopeId, scope); } return scope; }; return CustomStyle; }()); var win = window; function needsShim() { return !(win.CSS && win.CSS.supports && win.CSS.supports('color', 'var(--c)')); } if (!win.__stencil_cssshim && needsShim()) { win.__stencil_cssshim = new CustomStyle(win, document); }