1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 | const safeSelector = (selector) => {
|
16 | const placeholders = [];
|
17 | let index = 0;
|
18 | let content;
|
19 |
|
20 |
|
21 | selector = selector.replace(/(\[[^\]]*\])/g, (_, keep) => {
|
22 | const replaceBy = `__ph-${index}__`;
|
23 | placeholders.push(keep);
|
24 | index++;
|
25 | return replaceBy;
|
26 | });
|
27 |
|
28 |
|
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 | };
|
41 | const restoreSafeSelector = (placeholders, content) => {
|
42 | return content.replace(/__ph-(\d+)__/g, (_, index) => placeholders[+index]);
|
43 | };
|
44 | const _polyfillHost = '-shadowcsshost';
|
45 | const _polyfillSlotted = '-shadowcssslotted';
|
46 |
|
47 | const _polyfillHostContext = '-shadowcsscontext';
|
48 | const _parenSuffix = ')(?:\\((' + '(?:\\([^)(]*\\)|[^)(]*)+?' + ')\\))?([^,{]*)';
|
49 | const _cssColonHostRe = new RegExp('(' + _polyfillHost + _parenSuffix, 'gim');
|
50 | const _cssColonHostContextRe = new RegExp('(' + _polyfillHostContext + _parenSuffix, 'gim');
|
51 | const _cssColonSlottedRe = new RegExp('(' + _polyfillSlotted + _parenSuffix, 'gim');
|
52 | const _polyfillHostNoCombinator = _polyfillHost + '-no-combinator';
|
53 | const _polyfillHostNoCombinatorRe = /-shadowcsshost-no-combinator([^\s]*)/;
|
54 | const _shadowDOMSelectorsRe = [/::shadow/g, /::content/g];
|
55 | const _selectorReSuffix = '([>\\s~+[.,{:][\\s\\S]*)?$';
|
56 | const _polyfillHostRe = /-shadowcsshost/gim;
|
57 | const _colonHostRe = /:host/gim;
|
58 | const _colonSlottedRe = /::slotted/gim;
|
59 | const _colonHostContextRe = /:host-context/gim;
|
60 | const _commentRe = /\/\*\s*[\s\S]*?\*\//g;
|
61 | const stripComments = (input) => {
|
62 | return input.replace(_commentRe, '');
|
63 | };
|
64 | const _commentWithHashRe = /\/\*\s*#\s*source(Mapping)?URL=[\s\S]+?\*\//g;
|
65 | const extractCommentsWithHash = (input) => {
|
66 | return input.match(_commentWithHashRe) || [];
|
67 | };
|
68 | const _ruleRe = /(\s*)([^;\{\}]+?)(\s*)((?:{%BLOCK%}?\s*;?)|(?:\s*;))/g;
|
69 | const _curlyRe = /([{}])/g;
|
70 | const OPEN_CURLY = '{';
|
71 | const CLOSE_CURLY = '}';
|
72 | const BLOCK_PLACEHOLDER = '%BLOCK%';
|
73 | const 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 | };
|
94 | const 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 | };
|
130 | const insertPolyfillHostInCssText = (selector) => {
|
131 | selector = selector
|
132 | .replace(_colonHostContextRe, _polyfillHostContext)
|
133 | .replace(_colonHostRe, _polyfillHost)
|
134 | .replace(_colonSlottedRe, _polyfillSlotted);
|
135 | return selector;
|
136 | };
|
137 | const convertColonRule = (cssText, regExp, partReplacer) => {
|
138 |
|
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 | };
|
156 | const colonHostPartReplacer = (host, part, suffix) => {
|
157 | return host + part.replace(_polyfillHost, '') + suffix;
|
158 | };
|
159 | const convertColonHost = (cssText) => {
|
160 | return convertColonRule(cssText, _cssColonHostRe, colonHostPartReplacer);
|
161 | };
|
162 | const 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 | };
|
170 | const 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 | };
|
206 | const convertColonHostContext = (cssText) => {
|
207 | return convertColonRule(cssText, _cssColonHostContextRe, colonHostContextPartReplacer);
|
208 | };
|
209 | const convertShadowDOMSelectors = (cssText) => {
|
210 | return _shadowDOMSelectorsRe.reduce((result, pattern) => result.replace(pattern, ' '), cssText);
|
211 | };
|
212 | const 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 | };
|
218 | const selectorNeedsScoping = (selector, scopeSelector) => {
|
219 | const re = makeScopeMatcher(scopeSelector);
|
220 | return !re.test(selector);
|
221 | };
|
222 | const applySimpleSelectorScope = (selector, scopeSelector, hostSelector) => {
|
223 |
|
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 | };
|
237 | const 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 |
|
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 |
|
268 |
|
269 |
|
270 |
|
271 |
|
272 |
|
273 |
|
274 |
|
275 |
|
276 |
|
277 |
|
278 | const hasHost = selector.indexOf(_polyfillHostNoCombinator) > -1;
|
279 |
|
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 |
|
293 | return restoreSafeSelector(safeContent.placeholders, scopedSelector);
|
294 | };
|
295 | const 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 | };
|
311 | const 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 | };
|
328 | const 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 | };
|
345 | const 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 |
|
383 | export { scopeCss };
|