UNPKG

15.6 kBJavaScriptView Raw
1/*
2 Stencil Client Platform v1.17.3 | MIT Licensed | https://stenciljs.com
3 */
4/**
5 * @license
6 * Copyright Google Inc. All Rights Reserved.
7 *
8 * Use of this source code is governed by an MIT-style license that can be
9 * found in the LICENSE file at https://angular.io/license
10 *
11 * This file is a port of shadowCSS from webcomponents.js to TypeScript.
12 * https://github.com/webcomponents/webcomponentsjs/blob/4efecd7e0e/src/ShadowCSS/ShadowCSS.js
13 * https://github.com/angular/angular/blob/master/packages/compiler/src/shadow_css.ts
14 */
15const safeSelector = (selector) => {
16 const placeholders = [];
17 let index = 0;
18 let content;
19 // Replaces attribute selectors with placeholders.
20 // The WS in [attr="va lue"] would otherwise be interpreted as a selector separator.
21 selector = selector.replace(/(\[[^\]]*\])/g, (_, keep) => {
22 const replaceBy = `__ph-${index}__`;
23 placeholders.push(keep);
24 index++;
25 return replaceBy;
26 });
27 // Replaces the expression in `:nth-child(2n + 1)` with a placeholder.
28 // WS and "+" would otherwise be interpreted as selector separators.
29 content = selector.replace(/(:nth-[-\w]+)(\([^)]+\))/g, (_, pseudo, exp) => {
30 const replaceBy = `__ph-${index}__`;
31 placeholders.push(exp);
32 index++;
33 return pseudo + replaceBy;
34 });
35 const ss = {
36 content,
37 placeholders,
38 };
39 return ss;
40};
41const restoreSafeSelector = (placeholders, content) => {
42 return content.replace(/__ph-(\d+)__/g, (_, index) => placeholders[+index]);
43};
44const _polyfillHost = '-shadowcsshost';
45const _polyfillSlotted = '-shadowcssslotted';
46// note: :host-context pre-processed to -shadowcsshostcontext.
47const _polyfillHostContext = '-shadowcsscontext';
48const _parenSuffix = ')(?:\\((' + '(?:\\([^)(]*\\)|[^)(]*)+?' + ')\\))?([^,{]*)';
49const _cssColonHostRe = new RegExp('(' + _polyfillHost + _parenSuffix, 'gim');
50const _cssColonHostContextRe = new RegExp('(' + _polyfillHostContext + _parenSuffix, 'gim');
51const _cssColonSlottedRe = new RegExp('(' + _polyfillSlotted + _parenSuffix, 'gim');
52const _polyfillHostNoCombinator = _polyfillHost + '-no-combinator';
53const _polyfillHostNoCombinatorRe = /-shadowcsshost-no-combinator([^\s]*)/;
54const _shadowDOMSelectorsRe = [/::shadow/g, /::content/g];
55const _selectorReSuffix = '([>\\s~+[.,{:][\\s\\S]*)?$';
56const _polyfillHostRe = /-shadowcsshost/gim;
57const _colonHostRe = /:host/gim;
58const _colonSlottedRe = /::slotted/gim;
59const _colonHostContextRe = /:host-context/gim;
60const _commentRe = /\/\*\s*[\s\S]*?\*\//g;
61const stripComments = (input) => {
62 return input.replace(_commentRe, '');
63};
64const _commentWithHashRe = /\/\*\s*#\s*source(Mapping)?URL=[\s\S]+?\*\//g;
65const extractCommentsWithHash = (input) => {
66 return input.match(_commentWithHashRe) || [];
67};
68const _ruleRe = /(\s*)([^;\{\}]+?)(\s*)((?:{%BLOCK%}?\s*;?)|(?:\s*;))/g;
69const _curlyRe = /([{}])/g;
70const OPEN_CURLY = '{';
71const CLOSE_CURLY = '}';
72const BLOCK_PLACEHOLDER = '%BLOCK%';
73const processRules = (input, ruleCallback) => {
74 const inputWithEscapedBlocks = escapeBlocks(input);
75 let nextBlockIndex = 0;
76 return inputWithEscapedBlocks.escapedString.replace(_ruleRe, (...m) => {
77 const selector = m[2];
78 let content = '';
79 let suffix = m[4];
80 let contentPrefix = '';
81 if (suffix && suffix.startsWith('{' + BLOCK_PLACEHOLDER)) {
82 content = inputWithEscapedBlocks.blocks[nextBlockIndex++];
83 suffix = suffix.substring(BLOCK_PLACEHOLDER.length + 1);
84 contentPrefix = '{';
85 }
86 const cssRule = {
87 selector,
88 content,
89 };
90 const rule = ruleCallback(cssRule);
91 return `${m[1]}${rule.selector}${m[3]}${contentPrefix}${rule.content}${suffix}`;
92 });
93};
94const escapeBlocks = (input) => {
95 const inputParts = input.split(_curlyRe);
96 const resultParts = [];
97 const escapedBlocks = [];
98 let bracketCount = 0;
99 let currentBlockParts = [];
100 for (let partIndex = 0; partIndex < inputParts.length; partIndex++) {
101 const part = inputParts[partIndex];
102 if (part === CLOSE_CURLY) {
103 bracketCount--;
104 }
105 if (bracketCount > 0) {
106 currentBlockParts.push(part);
107 }
108 else {
109 if (currentBlockParts.length > 0) {
110 escapedBlocks.push(currentBlockParts.join(''));
111 resultParts.push(BLOCK_PLACEHOLDER);
112 currentBlockParts = [];
113 }
114 resultParts.push(part);
115 }
116 if (part === OPEN_CURLY) {
117 bracketCount++;
118 }
119 }
120 if (currentBlockParts.length > 0) {
121 escapedBlocks.push(currentBlockParts.join(''));
122 resultParts.push(BLOCK_PLACEHOLDER);
123 }
124 const strEscapedBlocks = {
125 escapedString: resultParts.join(''),
126 blocks: escapedBlocks,
127 };
128 return strEscapedBlocks;
129};
130const insertPolyfillHostInCssText = (selector) => {
131 selector = selector
132 .replace(_colonHostContextRe, _polyfillHostContext)
133 .replace(_colonHostRe, _polyfillHost)
134 .replace(_colonSlottedRe, _polyfillSlotted);
135 return selector;
136};
137const convertColonRule = (cssText, regExp, partReplacer) => {
138 // m[1] = :host(-context), m[2] = contents of (), m[3] rest of rule
139 return cssText.replace(regExp, (...m) => {
140 if (m[2]) {
141 const parts = m[2].split(',');
142 const r = [];
143 for (let i = 0; i < parts.length; i++) {
144 const p = parts[i].trim();
145 if (!p)
146 break;
147 r.push(partReplacer(_polyfillHostNoCombinator, p, m[3]));
148 }
149 return r.join(',');
150 }
151 else {
152 return _polyfillHostNoCombinator + m[3];
153 }
154 });
155};
156const colonHostPartReplacer = (host, part, suffix) => {
157 return host + part.replace(_polyfillHost, '') + suffix;
158};
159const convertColonHost = (cssText) => {
160 return convertColonRule(cssText, _cssColonHostRe, colonHostPartReplacer);
161};
162const colonHostContextPartReplacer = (host, part, suffix) => {
163 if (part.indexOf(_polyfillHost) > -1) {
164 return colonHostPartReplacer(host, part, suffix);
165 }
166 else {
167 return host + part + suffix + ', ' + part + ' ' + host + suffix;
168 }
169};
170const convertColonSlotted = (cssText, slotScopeId) => {
171 const slotClass = '.' + slotScopeId + ' > ';
172 const selectors = [];
173 cssText = cssText.replace(_cssColonSlottedRe, (...m) => {
174 if (m[2]) {
175 const compound = m[2].trim();
176 const suffix = m[3];
177 const slottedSelector = slotClass + compound + suffix;
178 let prefixSelector = '';
179 for (let i = m[4] - 1; i >= 0; i--) {
180 const char = m[5][i];
181 if (char === '}' || char === ',') {
182 break;
183 }
184 prefixSelector = char + prefixSelector;
185 }
186 const orgSelector = prefixSelector + slottedSelector;
187 const addedSelector = `${prefixSelector.trimRight()}${slottedSelector.trim()}`;
188 if (orgSelector.trim() !== addedSelector.trim()) {
189 const updatedSelector = `${addedSelector}, ${orgSelector}`;
190 selectors.push({
191 orgSelector,
192 updatedSelector,
193 });
194 }
195 return slottedSelector;
196 }
197 else {
198 return _polyfillHostNoCombinator + m[3];
199 }
200 });
201 return {
202 selectors,
203 cssText,
204 };
205};
206const convertColonHostContext = (cssText) => {
207 return convertColonRule(cssText, _cssColonHostContextRe, colonHostContextPartReplacer);
208};
209const convertShadowDOMSelectors = (cssText) => {
210 return _shadowDOMSelectorsRe.reduce((result, pattern) => result.replace(pattern, ' '), cssText);
211};
212const makeScopeMatcher = (scopeSelector) => {
213 const lre = /\[/g;
214 const rre = /\]/g;
215 scopeSelector = scopeSelector.replace(lre, '\\[').replace(rre, '\\]');
216 return new RegExp('^(' + scopeSelector + ')' + _selectorReSuffix, 'm');
217};
218const selectorNeedsScoping = (selector, scopeSelector) => {
219 const re = makeScopeMatcher(scopeSelector);
220 return !re.test(selector);
221};
222const applySimpleSelectorScope = (selector, scopeSelector, hostSelector) => {
223 // In Android browser, the lastIndex is not reset when the regex is used in String.replace()
224 _polyfillHostRe.lastIndex = 0;
225 if (_polyfillHostRe.test(selector)) {
226 const replaceBy = `.${hostSelector}`;
227 return selector
228 .replace(_polyfillHostNoCombinatorRe, (_, selector) => {
229 return selector.replace(/([^:]*)(:*)(.*)/, (_, before, colon, after) => {
230 return before + replaceBy + colon + after;
231 });
232 })
233 .replace(_polyfillHostRe, replaceBy + ' ');
234 }
235 return scopeSelector + ' ' + selector;
236};
237const applyStrictSelectorScope = (selector, scopeSelector, hostSelector) => {
238 const isRe = /\[is=([^\]]*)\]/g;
239 scopeSelector = scopeSelector.replace(isRe, (_, ...parts) => parts[0]);
240 const className = '.' + scopeSelector;
241 const _scopeSelectorPart = (p) => {
242 let scopedP = p.trim();
243 if (!scopedP) {
244 return '';
245 }
246 if (p.indexOf(_polyfillHostNoCombinator) > -1) {
247 scopedP = applySimpleSelectorScope(p, scopeSelector, hostSelector);
248 }
249 else {
250 // remove :host since it should be unnecessary
251 const t = p.replace(_polyfillHostRe, '');
252 if (t.length > 0) {
253 const matches = t.match(/([^:]*)(:*)(.*)/);
254 if (matches) {
255 scopedP = matches[1] + className + matches[2] + matches[3];
256 }
257 }
258 }
259 return scopedP;
260 };
261 const safeContent = safeSelector(selector);
262 selector = safeContent.content;
263 let scopedSelector = '';
264 let startIndex = 0;
265 let res;
266 const sep = /( |>|\+|~(?!=))\s*/g;
267 // If a selector appears before :host it should not be shimmed as it
268 // matches on ancestor elements and not on elements in the host's shadow
269 // `:host-context(div)` is transformed to
270 // `-shadowcsshost-no-combinatordiv, div -shadowcsshost-no-combinator`
271 // the `div` is not part of the component in the 2nd selectors and should not be scoped.
272 // Historically `component-tag:host` was matching the component so we also want to preserve
273 // this behavior to avoid breaking legacy apps (it should not match).
274 // The behavior should be:
275 // - `tag:host` -> `tag[h]` (this is to avoid breaking legacy apps, should not match anything)
276 // - `tag :host` -> `tag [h]` (`tag` is not scoped because it's considered part of a
277 // `:host-context(tag)`)
278 const hasHost = selector.indexOf(_polyfillHostNoCombinator) > -1;
279 // Only scope parts after the first `-shadowcsshost-no-combinator` when it is present
280 let shouldScope = !hasHost;
281 while ((res = sep.exec(selector)) !== null) {
282 const separator = res[1];
283 const part = selector.slice(startIndex, res.index).trim();
284 shouldScope = shouldScope || part.indexOf(_polyfillHostNoCombinator) > -1;
285 const scopedPart = shouldScope ? _scopeSelectorPart(part) : part;
286 scopedSelector += `${scopedPart} ${separator} `;
287 startIndex = sep.lastIndex;
288 }
289 const part = selector.substring(startIndex);
290 shouldScope = shouldScope || part.indexOf(_polyfillHostNoCombinator) > -1;
291 scopedSelector += shouldScope ? _scopeSelectorPart(part) : part;
292 // replace the placeholders with their original values
293 return restoreSafeSelector(safeContent.placeholders, scopedSelector);
294};
295const scopeSelector = (selector, scopeSelectorText, hostSelector, slotSelector) => {
296 return selector
297 .split(',')
298 .map(shallowPart => {
299 if (slotSelector && shallowPart.indexOf('.' + slotSelector) > -1) {
300 return shallowPart.trim();
301 }
302 if (selectorNeedsScoping(shallowPart, scopeSelectorText)) {
303 return applyStrictSelectorScope(shallowPart, scopeSelectorText, hostSelector).trim();
304 }
305 else {
306 return shallowPart.trim();
307 }
308 })
309 .join(', ');
310};
311const scopeSelectors = (cssText, scopeSelectorText, hostSelector, slotSelector, commentOriginalSelector) => {
312 return processRules(cssText, (rule) => {
313 let selector = rule.selector;
314 let content = rule.content;
315 if (rule.selector[0] !== '@') {
316 selector = scopeSelector(rule.selector, scopeSelectorText, hostSelector, slotSelector);
317 }
318 else if (rule.selector.startsWith('@media') || rule.selector.startsWith('@supports') || rule.selector.startsWith('@page') || rule.selector.startsWith('@document')) {
319 content = scopeSelectors(rule.content, scopeSelectorText, hostSelector, slotSelector);
320 }
321 const cssRule = {
322 selector: selector.replace(/\s{2,}/g, ' ').trim(),
323 content,
324 };
325 return cssRule;
326 });
327};
328const scopeCssText = (cssText, scopeId, hostScopeId, slotScopeId, commentOriginalSelector) => {
329 cssText = insertPolyfillHostInCssText(cssText);
330 cssText = convertColonHost(cssText);
331 cssText = convertColonHostContext(cssText);
332 const slotted = convertColonSlotted(cssText, slotScopeId);
333 cssText = slotted.cssText;
334 cssText = convertShadowDOMSelectors(cssText);
335 if (scopeId) {
336 cssText = scopeSelectors(cssText, scopeId, hostScopeId, slotScopeId);
337 }
338 cssText = cssText.replace(/-shadowcsshost-no-combinator/g, `.${hostScopeId}`);
339 cssText = cssText.replace(/>\s*\*\s+([^{, ]+)/gm, ' $1 ');
340 return {
341 cssText: cssText.trim(),
342 slottedSelectors: slotted.selectors,
343 };
344};
345const scopeCss = (cssText, scopeId, commentOriginalSelector) => {
346 const hostScopeId = scopeId + '-h';
347 const slotScopeId = scopeId + '-s';
348 const commentsWithHash = extractCommentsWithHash(cssText);
349 cssText = stripComments(cssText);
350 const orgSelectors = [];
351 if (commentOriginalSelector) {
352 const processCommentedSelector = (rule) => {
353 const placeholder = `/*!@___${orgSelectors.length}___*/`;
354 const comment = `/*!@${rule.selector}*/`;
355 orgSelectors.push({ placeholder, comment });
356 rule.selector = placeholder + rule.selector;
357 return rule;
358 };
359 cssText = processRules(cssText, rule => {
360 if (rule.selector[0] !== '@') {
361 return processCommentedSelector(rule);
362 }
363 else if (rule.selector.startsWith('@media') || rule.selector.startsWith('@supports') || rule.selector.startsWith('@page') || rule.selector.startsWith('@document')) {
364 rule.content = processRules(rule.content, processCommentedSelector);
365 return rule;
366 }
367 return rule;
368 });
369 }
370 const scoped = scopeCssText(cssText, scopeId, hostScopeId, slotScopeId);
371 cssText = [scoped.cssText, ...commentsWithHash].join('\n');
372 if (commentOriginalSelector) {
373 orgSelectors.forEach(({ placeholder, comment }) => {
374 cssText = cssText.replace(placeholder, comment);
375 });
376 }
377 scoped.slottedSelectors.forEach(slottedSelector => {
378 cssText = cssText.replace(slottedSelector.orgSelector, slottedSelector.updatedSelector);
379 });
380 return cssText;
381};
382
383export { scopeCss };