UNPKG

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