UNPKG

21.9 kBJavaScriptView Raw
1/*
2Extremely simple css parser. Intended to be not more than what we need
3and definitely not necessarily correct =).
4*/
5/** @unrestricted */
6var StyleNode = /** @class */ (function () {
7 function StyleNode() {
8 this.start = 0;
9 this.end = 0;
10 this.previous = null;
11 this.parent = null;
12 this.rules = null;
13 this.parsedCssText = '';
14 this.cssText = '';
15 this.atRule = false;
16 this.type = 0;
17 this.keyframesName = '';
18 this.selector = '';
19 this.parsedSelector = '';
20 }
21 return StyleNode;
22}());
23// given a string of css, return a simple rule tree
24/**
25 * @param {string} text
26 * @return {StyleNode}
27 */
28function parse(text) {
29 text = clean(text);
30 return parseCss(lex(text), text);
31}
32// remove stuff we don't care about that may hinder parsing
33/**
34 * @param {string} cssText
35 * @return {string}
36 */
37function clean(cssText) {
38 return cssText.replace(RX.comments, '').replace(RX.port, '');
39}
40// super simple {...} lexer that returns a node tree
41/**
42 * @param {string} text
43 * @return {StyleNode}
44 */
45function lex(text) {
46 var root = new StyleNode();
47 root['start'] = 0;
48 root['end'] = text.length;
49 var n = root;
50 for (var i = 0, l = text.length; i < l; i++) {
51 if (text[i] === OPEN_BRACE) {
52 if (!n['rules']) {
53 n['rules'] = [];
54 }
55 var p = n;
56 var previous = p['rules'][p['rules'].length - 1] || null;
57 n = new StyleNode();
58 n['start'] = i + 1;
59 n['parent'] = p;
60 n['previous'] = previous;
61 p['rules'].push(n);
62 }
63 else if (text[i] === CLOSE_BRACE) {
64 n['end'] = i + 1;
65 n = n['parent'] || root;
66 }
67 }
68 return root;
69}
70// add selectors/cssText to node tree
71/**
72 * @param {StyleNode} node
73 * @param {string} text
74 * @return {StyleNode}
75 */
76function parseCss(node, text) {
77 var t = text.substring(node['start'], node['end'] - 1);
78 node['parsedCssText'] = node['cssText'] = t.trim();
79 if (node.parent) {
80 var ss = node.previous ? node.previous['end'] : node.parent['start'];
81 t = text.substring(ss, node['start'] - 1);
82 t = _expandUnicodeEscapes(t);
83 t = t.replace(RX.multipleSpaces, ' ');
84 // TODO(sorvell): ad hoc; make selector include only after last ;
85 // helps with mixin syntax
86 t = t.substring(t.lastIndexOf(';') + 1);
87 var s = node['parsedSelector'] = node['selector'] = t.trim();
88 node['atRule'] = (s.indexOf(AT_START) === 0);
89 // note, support a subset of rule types...
90 if (node['atRule']) {
91 if (s.indexOf(MEDIA_START) === 0) {
92 node['type'] = types.MEDIA_RULE;
93 }
94 else if (s.match(RX.keyframesRule)) {
95 node['type'] = types.KEYFRAMES_RULE;
96 node['keyframesName'] = node['selector'].split(RX.multipleSpaces).pop();
97 }
98 }
99 else {
100 if (s.indexOf(VAR_START) === 0) {
101 node['type'] = types.MIXIN_RULE;
102 }
103 else {
104 node['type'] = types.STYLE_RULE;
105 }
106 }
107 }
108 var r$ = node['rules'];
109 if (r$) {
110 for (var i = 0, l = r$.length, r = void 0; (i < l) && (r = r$[i]); i++) {
111 parseCss(r, text);
112 }
113 }
114 return node;
115}
116/**
117 * conversion of sort unicode escapes with spaces like `\33 ` (and longer) into
118 * expanded form that doesn't require trailing space `\000033`
119 * @param {string} s
120 * @return {string}
121 */
122function _expandUnicodeEscapes(s) {
123 return s.replace(/\\([0-9a-f]{1,6})\s/gi, function () {
124 var code = arguments[1], repeat = 6 - code.length;
125 while (repeat--) {
126 code = '0' + code;
127 }
128 return '\\' + code;
129 });
130}
131/** @enum {number} */
132var types = {
133 STYLE_RULE: 1,
134 KEYFRAMES_RULE: 7,
135 MEDIA_RULE: 4,
136 MIXIN_RULE: 1000
137};
138var OPEN_BRACE = '{';
139var CLOSE_BRACE = '}';
140// helper regexp's
141var RX = {
142 comments: /\/\*[^*]*\*+([^/*][^*]*\*+)*\//gim,
143 port: /@import[^;]*;/gim,
144 customProp: /(?:^[^;\-\s}]+)?--[^;{}]*?:[^{};]*?(?:[;\n]|$)/gim,
145 mixinProp: /(?:^[^;\-\s}]+)?--[^;{}]*?:[^{};]*?{[^}]*?}(?:[;\n]|$)?/gim,
146 mixinApply: /@apply\s*\(?[^);]*\)?\s*(?:[;\n]|$)?/gim,
147 varApply: /[^;:]*?:[^;]*?var\([^;]*\)(?:[;\n]|$)?/gim,
148 keyframesRule: /^@[^\s]*keyframes/,
149 multipleSpaces: /\s+/g
150};
151var VAR_START = '--';
152var MEDIA_START = '@media';
153var AT_START = '@';
154function findRegex(regex, cssText, offset) {
155 regex['lastIndex'] = 0;
156 var r = cssText.substring(offset).match(regex);
157 if (r) {
158 var start = offset + r['index'];
159 return {
160 start: start,
161 end: start + r[0].length
162 };
163 }
164 return null;
165}
166var VAR_USAGE_START = /\bvar\(/;
167var VAR_ASSIGN_START = /\B--[\w-]+\s*:/;
168var COMMENTS = /\/\*[^*]*\*+([^/*][^*]*\*+)*\//gim;
169var TRAILING_LINES = /^[\t ]+\n/gm;
170function resolveVar(props, prop, fallback) {
171 if (props[prop]) {
172 return props[prop];
173 }
174 if (fallback) {
175 return executeTemplate(fallback, props);
176 }
177 return '';
178}
179function findVarEndIndex(cssText, offset) {
180 var count = 0;
181 var i = offset;
182 for (; i < cssText.length; i++) {
183 var c = cssText[i];
184 if (c === '(') {
185 count++;
186 }
187 else if (c === ')') {
188 count--;
189 if (count <= 0) {
190 return i + 1;
191 }
192 }
193 }
194 return i;
195}
196function parseVar(cssText, offset) {
197 var varPos = findRegex(VAR_USAGE_START, cssText, offset);
198 if (!varPos) {
199 return null;
200 }
201 var endVar = findVarEndIndex(cssText, varPos.start);
202 var varContent = cssText.substring(varPos.end, endVar - 1);
203 var _a = varContent.split(','), propName = _a[0], fallback = _a.slice(1);
204 return {
205 start: varPos.start,
206 end: endVar,
207 propName: propName.trim(),
208 fallback: fallback.length > 0 ? fallback.join(',').trim() : undefined
209 };
210}
211function compileVar(cssText, template, offset) {
212 var varMeta = parseVar(cssText, offset);
213 if (!varMeta) {
214 template.push(cssText.substring(offset, cssText.length));
215 return cssText.length;
216 }
217 var propName = varMeta.propName;
218 var fallback = varMeta.fallback != null ? compileTemplate(varMeta.fallback) : undefined;
219 template.push(cssText.substring(offset, varMeta.start), function (params) { return resolveVar(params, propName, fallback); });
220 return varMeta.end;
221}
222function executeTemplate(template, props) {
223 var final = '';
224 for (var i = 0; i < template.length; i++) {
225 var s = template[i];
226 final += (typeof s === 'string')
227 ? s
228 : s(props);
229 }
230 return final;
231}
232function findEndValue(cssText, offset) {
233 var onStr = false;
234 var double = false;
235 var i = offset;
236 for (; i < cssText.length; i++) {
237 var c = cssText[i];
238 if (onStr) {
239 if (double && c === '"') {
240 onStr = false;
241 }
242 if (!double && c === '\'') {
243 onStr = false;
244 }
245 }
246 else {
247 if (c === '"') {
248 onStr = true;
249 double = true;
250 }
251 else if (c === '\'') {
252 onStr = true;
253 double = false;
254 }
255 else if (c === ';') {
256 return i + 1;
257 }
258 else if (c === '}') {
259 return i;
260 }
261 }
262 }
263 return i;
264}
265function removeCustomAssigns(cssText) {
266 var final = '';
267 var offset = 0;
268 while (true) {
269 var assignPos = findRegex(VAR_ASSIGN_START, cssText, offset);
270 var start = assignPos ? assignPos.start : cssText.length;
271 final += cssText.substring(offset, start);
272 if (assignPos) {
273 offset = findEndValue(cssText, start);
274 }
275 else {
276 break;
277 }
278 }
279 return final;
280}
281function compileTemplate(cssText) {
282 var index = 0;
283 cssText = cssText.replace(COMMENTS, '');
284 cssText = removeCustomAssigns(cssText)
285 .replace(TRAILING_LINES, '');
286 var segments = [];
287 while (index < cssText.length) {
288 index = compileVar(cssText, segments, index);
289 }
290 return segments;
291}
292function resolveValues(selectors) {
293 var props = {};
294 selectors.forEach(function (selector) {
295 selector.declarations.forEach(function (dec) {
296 props[dec.prop] = dec.value;
297 });
298 });
299 var propsValues = {};
300 var entries = Object.entries(props);
301 var _loop_1 = function (i) {
302 var dirty = false;
303 entries.forEach(function (_a) {
304 var key = _a[0], value = _a[1];
305 var propValue = executeTemplate(value, propsValues);
306 if (propValue !== propsValues[key]) {
307 propsValues[key] = propValue;
308 dirty = true;
309 }
310 });
311 if (!dirty) {
312 return "break";
313 }
314 };
315 for (var i = 0; i < 10; i++) {
316 var state_1 = _loop_1();
317 if (state_1 === "break")
318 break;
319 }
320 return propsValues;
321}
322function getSelectors(root, index) {
323 if (index === void 0) { index = 0; }
324 if (!root.rules) {
325 return [];
326 }
327 var selectors = [];
328 root.rules
329 .filter(function (rule) { return rule.type === types.STYLE_RULE; })
330 .forEach(function (rule) {
331 var declarations = getDeclarations(rule.cssText);
332 if (declarations.length > 0) {
333 rule.parsedSelector.split(',').forEach(function (selector) {
334 selector = selector.trim();
335 selectors.push({
336 selector: selector,
337 declarations: declarations,
338 specificity: computeSpecificity(),
339 nu: index
340 });
341 });
342 }
343 index++;
344 });
345 return selectors;
346}
347function computeSpecificity(_selector) {
348 return 1;
349}
350var IMPORTANT = '!important';
351var FIND_DECLARATIONS = /(?:^|[;\s{]\s*)(--[\w-]*?)\s*:\s*(?:((?:'(?:\\'|.)*?'|"(?:\\"|.)*?"|\([^)]*?\)|[^};{])+)|\{([^}]*)\}(?:(?=[;\s}])|$))/gm;
352function getDeclarations(cssText) {
353 var declarations = [];
354 var xArray;
355 while (xArray = FIND_DECLARATIONS.exec(cssText.trim())) {
356 var _a = normalizeValue(xArray[2]), value = _a.value, important = _a.important;
357 declarations.push({
358 prop: xArray[1].trim(),
359 value: compileTemplate(value),
360 important: important,
361 });
362 }
363 return declarations;
364}
365function normalizeValue(value) {
366 var regex = /\s+/gim;
367 value = value.replace(regex, ' ').trim();
368 var important = value.endsWith(IMPORTANT);
369 if (important) {
370 value = value.substr(0, value.length - IMPORTANT.length).trim();
371 }
372 return {
373 value: value,
374 important: important
375 };
376}
377function getActiveSelectors(hostEl, hostScopeMap, globalScopes) {
378 // computes the css scopes that might affect this particular element
379 // avoiding using spread arrays to avoid ts helper fns when in es5
380 var scopes = [];
381 var scopesForElement = getScopesForElement(hostScopeMap, hostEl);
382 // globalScopes are always took into account
383 globalScopes.forEach(function (s) { return scopes.push(s); });
384 // the parent scopes are computed by walking parent dom until <html> is reached
385 scopesForElement.forEach(function (s) { return scopes.push(s); });
386 // each scope might have an array of associated selectors
387 // let's flatten the complete array of selectors from all the scopes
388 var selectorSet = getSelectorsForScopes(scopes);
389 // we filter to only the selectors that matches the hostEl
390 var activeSelectors = selectorSet.filter(function (selector) { return matches(hostEl, selector.selector); });
391 // sort selectors by specifity
392 return sortSelectors(activeSelectors);
393}
394function getScopesForElement(hostTemplateMap, node) {
395 var scopes = [];
396 while (node) {
397 var scope = hostTemplateMap.get(node);
398 if (scope) {
399 scopes.push(scope);
400 }
401 node = node.parentElement;
402 }
403 return scopes;
404}
405function getSelectorsForScopes(scopes) {
406 var selectors = [];
407 scopes.forEach(function (scope) {
408 selectors.push.apply(selectors, scope.selectors);
409 });
410 return selectors;
411}
412function sortSelectors(selectors) {
413 selectors.sort(function (a, b) {
414 if (a.specificity === b.specificity) {
415 return a.nu - b.nu;
416 }
417 return a.specificity - b.specificity;
418 });
419 return selectors;
420}
421function matches(el, selector) {
422 return selector === ':root' || selector === 'html' || el.matches(selector);
423}
424function parseCSS(original) {
425 var ast = parse(original);
426 var template = compileTemplate(original);
427 var selectors = getSelectors(ast);
428 return {
429 original: original,
430 template: template,
431 selectors: selectors,
432 usesCssVars: template.length > 1
433 };
434}
435function addGlobalStyle(globalScopes, styleEl) {
436 if (globalScopes.some(function (css) { return css.styleEl === styleEl; })) {
437 return false;
438 }
439 var css = parseCSS(styleEl.textContent);
440 css.styleEl = styleEl;
441 globalScopes.push(css);
442 return true;
443}
444function updateGlobalScopes(scopes) {
445 var selectors = getSelectorsForScopes(scopes);
446 var props = resolveValues(selectors);
447 scopes.forEach(function (scope) {
448 if (scope.usesCssVars) {
449 scope.styleEl.textContent = executeTemplate(scope.template, props);
450 }
451 });
452}
453function reScope(scope, scopeId) {
454 var template = scope.template.map(function (segment) {
455 return (typeof segment === 'string')
456 ? replaceScope(segment, scope.scopeId, scopeId)
457 : segment;
458 });
459 var selectors = scope.selectors.map(function (sel) {
460 return Object.assign(Object.assign({}, sel), { selector: replaceScope(sel.selector, scope.scopeId, scopeId) });
461 });
462 return Object.assign(Object.assign({}, scope), { template: template,
463 selectors: selectors,
464 scopeId: scopeId });
465}
466function replaceScope(original, oldScopeId, newScopeId) {
467 original = replaceAll(original, "\\." + oldScopeId, "." + newScopeId);
468 return original;
469}
470function replaceAll(input, find, replace) {
471 return input.replace(new RegExp(find, 'g'), replace);
472}
473function loadDocument(doc, globalScopes) {
474 loadDocumentStyles(doc, globalScopes);
475 return loadDocumentLinks(doc, globalScopes);
476}
477function startWatcher(doc, globalScopes) {
478 var mutation = new MutationObserver(function () {
479 if (loadDocumentStyles(doc, globalScopes)) {
480 updateGlobalScopes(globalScopes);
481 }
482 });
483 mutation.observe(document.head, { childList: true });
484}
485function loadDocumentLinks(doc, globalScopes) {
486 var promises = [];
487 var linkElms = doc.querySelectorAll('link[rel="stylesheet"][href]:not([data-no-shim])');
488 for (var i = 0; i < linkElms.length; i++) {
489 promises.push(addGlobalLink(doc, globalScopes, linkElms[i]));
490 }
491 return Promise.all(promises);
492}
493function loadDocumentStyles(doc, globalScopes) {
494 var styleElms = Array.from(doc.querySelectorAll('style:not([data-styles]):not([data-no-shim])'));
495 return styleElms
496 .map(function (style) { return addGlobalStyle(globalScopes, style); })
497 .some(Boolean);
498}
499function addGlobalLink(doc, globalScopes, linkElm) {
500 var url = linkElm.href;
501 return fetch(url).then(function (rsp) { return rsp.text(); }).then(function (text) {
502 if (hasCssVariables(text) && linkElm.parentNode) {
503 if (hasRelativeUrls(text)) {
504 text = fixRelativeUrls(text, url);
505 }
506 var styleEl = doc.createElement('style');
507 styleEl.setAttribute('data-styles', '');
508 styleEl.textContent = text;
509 addGlobalStyle(globalScopes, styleEl);
510 linkElm.parentNode.insertBefore(styleEl, linkElm);
511 linkElm.remove();
512 }
513 }).catch(function (err) {
514 console.error(err);
515 });
516}
517// This regexp tries to determine when a variable is declared, for example:
518//
519// .my-el { --highlight-color: green; }
520//
521// but we don't want to trigger when a classname uses "--" or a pseudo-class is
522// used. We assume that the only characters that can preceed a variable
523// declaration are "{", from an opening block, ";" from a preceeding rule, or a
524// space. This prevents the regexp from matching a word in a selector, since
525// they would need to start with a "." or "#". (We assume element names don't
526// start with "--").
527var CSS_VARIABLE_REGEXP = /[\s;{]--[-a-zA-Z0-9]+\s*:/m;
528function hasCssVariables(css) {
529 return css.indexOf('var(') > -1 || CSS_VARIABLE_REGEXP.test(css);
530}
531// This regexp find all url() usages with relative urls
532var CSS_URL_REGEXP = /url[\s]*\([\s]*['"]?(?!(?:https?|data)\:|\/)([^\'\"\)]*)[\s]*['"]?\)[\s]*/gim;
533function hasRelativeUrls(css) {
534 CSS_URL_REGEXP.lastIndex = 0;
535 return CSS_URL_REGEXP.test(css);
536}
537function fixRelativeUrls(css, originalUrl) {
538 // get the basepath from the original import url
539 var basePath = originalUrl.replace(/[^/]*$/, '');
540 // replace the relative url, with the new relative url
541 return css.replace(CSS_URL_REGEXP, function (fullMatch, url) {
542 // rhe new relative path is the base path + uri
543 // TODO: normalize relative URL
544 var relativeUrl = basePath + url;
545 return fullMatch.replace(url, relativeUrl);
546 });
547}
548var CustomStyle = /** @class */ (function () {
549 function CustomStyle(win, doc) {
550 this.win = win;
551 this.doc = doc;
552 this.count = 0;
553 this.hostStyleMap = new WeakMap();
554 this.hostScopeMap = new WeakMap();
555 this.globalScopes = [];
556 this.scopesMap = new Map();
557 this.didInit = false;
558 }
559 CustomStyle.prototype.initShim = function () {
560 var _this = this;
561 if (this.didInit) {
562 return Promise.resolve();
563 }
564 else {
565 this.didInit = true;
566 return new Promise(function (resolve) {
567 _this.win.requestAnimationFrame(function () {
568 startWatcher(_this.doc, _this.globalScopes);
569 loadDocument(_this.doc, _this.globalScopes).then(function () { return resolve(); });
570 });
571 });
572 }
573 };
574 CustomStyle.prototype.addLink = function (linkEl) {
575 var _this = this;
576 return addGlobalLink(this.doc, this.globalScopes, linkEl).then(function () {
577 _this.updateGlobal();
578 });
579 };
580 CustomStyle.prototype.addGlobalStyle = function (styleEl) {
581 if (addGlobalStyle(this.globalScopes, styleEl)) {
582 this.updateGlobal();
583 }
584 };
585 CustomStyle.prototype.createHostStyle = function (hostEl, cssScopeId, cssText, isScoped) {
586 if (this.hostScopeMap.has(hostEl)) {
587 throw new Error('host style already created');
588 }
589 var baseScope = this.registerHostTemplate(cssText, cssScopeId, isScoped);
590 var styleEl = this.doc.createElement('style');
591 styleEl.setAttribute('data-no-shim', '');
592 if (!baseScope.usesCssVars) {
593 // This component does not use (read) css variables
594 styleEl.textContent = cssText;
595 }
596 else if (isScoped) {
597 // This component is dynamic: uses css var and is scoped
598 styleEl['s-sc'] = cssScopeId = baseScope.scopeId + "-" + this.count;
599 styleEl.textContent = '/*needs update*/';
600 this.hostStyleMap.set(hostEl, styleEl);
601 this.hostScopeMap.set(hostEl, reScope(baseScope, cssScopeId));
602 this.count++;
603 }
604 else {
605 // This component uses css vars, but it's no-encapsulation (global static)
606 baseScope.styleEl = styleEl;
607 if (!baseScope.usesCssVars) {
608 styleEl.textContent = executeTemplate(baseScope.template, {});
609 }
610 this.globalScopes.push(baseScope);
611 this.updateGlobal();
612 this.hostScopeMap.set(hostEl, baseScope);
613 }
614 return styleEl;
615 };
616 CustomStyle.prototype.removeHost = function (hostEl) {
617 var css = this.hostStyleMap.get(hostEl);
618 if (css) {
619 css.remove();
620 }
621 this.hostStyleMap.delete(hostEl);
622 this.hostScopeMap.delete(hostEl);
623 };
624 CustomStyle.prototype.updateHost = function (hostEl) {
625 var scope = this.hostScopeMap.get(hostEl);
626 if (scope && scope.usesCssVars && scope.isScoped) {
627 var styleEl = this.hostStyleMap.get(hostEl);
628 if (styleEl) {
629 var selectors = getActiveSelectors(hostEl, this.hostScopeMap, this.globalScopes);
630 var props = resolveValues(selectors);
631 styleEl.textContent = executeTemplate(scope.template, props);
632 }
633 }
634 };
635 CustomStyle.prototype.updateGlobal = function () {
636 updateGlobalScopes(this.globalScopes);
637 };
638 CustomStyle.prototype.registerHostTemplate = function (cssText, scopeId, isScoped) {
639 var scope = this.scopesMap.get(scopeId);
640 if (!scope) {
641 scope = parseCSS(cssText);
642 scope.scopeId = scopeId;
643 scope.isScoped = isScoped;
644 this.scopesMap.set(scopeId, scope);
645 }
646 return scope;
647 };
648 return CustomStyle;
649}());
650var win = window;
651function needsShim() {
652 return !(win.CSS && win.CSS.supports && win.CSS.supports('color', 'var(--c)'));
653}
654if (!win.__stencil_cssshim && needsShim()) {
655 win.__stencil_cssshim = new CustomStyle(win, document);
656}