shadow-css.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. /*
  2. Stencil Client Platform v2.15.1 | 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. */
  15. const 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. };
  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. // note: :host-context pre-processed to -shadowcsshostcontext.
  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 _selectorPartsRe = /(^.*?[^\\])??((:+)(.*)|$)/;
  71. const OPEN_CURLY = '{';
  72. const CLOSE_CURLY = '}';
  73. const BLOCK_PLACEHOLDER = '%BLOCK%';
  74. const processRules = (input, ruleCallback) => {
  75. const inputWithEscapedBlocks = escapeBlocks(input);
  76. let nextBlockIndex = 0;
  77. return inputWithEscapedBlocks.escapedString.replace(_ruleRe, (...m) => {
  78. const selector = m[2];
  79. let content = '';
  80. let suffix = m[4];
  81. let contentPrefix = '';
  82. if (suffix && suffix.startsWith('{' + BLOCK_PLACEHOLDER)) {
  83. content = inputWithEscapedBlocks.blocks[nextBlockIndex++];
  84. suffix = suffix.substring(BLOCK_PLACEHOLDER.length + 1);
  85. contentPrefix = '{';
  86. }
  87. const cssRule = {
  88. selector,
  89. content,
  90. };
  91. const rule = ruleCallback(cssRule);
  92. return `${m[1]}${rule.selector}${m[3]}${contentPrefix}${rule.content}${suffix}`;
  93. });
  94. };
  95. const escapeBlocks = (input) => {
  96. const inputParts = input.split(_curlyRe);
  97. const resultParts = [];
  98. const escapedBlocks = [];
  99. let bracketCount = 0;
  100. let currentBlockParts = [];
  101. for (let partIndex = 0; partIndex < inputParts.length; partIndex++) {
  102. const part = inputParts[partIndex];
  103. if (part === CLOSE_CURLY) {
  104. bracketCount--;
  105. }
  106. if (bracketCount > 0) {
  107. currentBlockParts.push(part);
  108. }
  109. else {
  110. if (currentBlockParts.length > 0) {
  111. escapedBlocks.push(currentBlockParts.join(''));
  112. resultParts.push(BLOCK_PLACEHOLDER);
  113. currentBlockParts = [];
  114. }
  115. resultParts.push(part);
  116. }
  117. if (part === OPEN_CURLY) {
  118. bracketCount++;
  119. }
  120. }
  121. if (currentBlockParts.length > 0) {
  122. escapedBlocks.push(currentBlockParts.join(''));
  123. resultParts.push(BLOCK_PLACEHOLDER);
  124. }
  125. const strEscapedBlocks = {
  126. escapedString: resultParts.join(''),
  127. blocks: escapedBlocks,
  128. };
  129. return strEscapedBlocks;
  130. };
  131. const insertPolyfillHostInCssText = (selector) => {
  132. selector = selector
  133. .replace(_colonHostContextRe, _polyfillHostContext)
  134. .replace(_colonHostRe, _polyfillHost)
  135. .replace(_colonSlottedRe, _polyfillSlotted);
  136. return selector;
  137. };
  138. const convertColonRule = (cssText, regExp, partReplacer) => {
  139. // m[1] = :host(-context), m[2] = contents of (), m[3] rest of rule
  140. return cssText.replace(regExp, (...m) => {
  141. if (m[2]) {
  142. const parts = m[2].split(',');
  143. const r = [];
  144. for (let i = 0; i < parts.length; i++) {
  145. const p = parts[i].trim();
  146. if (!p)
  147. break;
  148. r.push(partReplacer(_polyfillHostNoCombinator, p, m[3]));
  149. }
  150. return r.join(',');
  151. }
  152. else {
  153. return _polyfillHostNoCombinator + m[3];
  154. }
  155. });
  156. };
  157. const colonHostPartReplacer = (host, part, suffix) => {
  158. return host + part.replace(_polyfillHost, '') + suffix;
  159. };
  160. const convertColonHost = (cssText) => {
  161. return convertColonRule(cssText, _cssColonHostRe, colonHostPartReplacer);
  162. };
  163. const colonHostContextPartReplacer = (host, part, suffix) => {
  164. if (part.indexOf(_polyfillHost) > -1) {
  165. return colonHostPartReplacer(host, part, suffix);
  166. }
  167. else {
  168. return host + part + suffix + ', ' + part + ' ' + host + suffix;
  169. }
  170. };
  171. const convertColonSlotted = (cssText, slotScopeId) => {
  172. const slotClass = '.' + slotScopeId + ' > ';
  173. const selectors = [];
  174. cssText = cssText.replace(_cssColonSlottedRe, (...m) => {
  175. if (m[2]) {
  176. const compound = m[2].trim();
  177. const suffix = m[3];
  178. const slottedSelector = slotClass + compound + suffix;
  179. let prefixSelector = '';
  180. for (let i = m[4] - 1; i >= 0; i--) {
  181. const char = m[5][i];
  182. if (char === '}' || char === ',') {
  183. break;
  184. }
  185. prefixSelector = char + prefixSelector;
  186. }
  187. const orgSelector = prefixSelector + slottedSelector;
  188. const addedSelector = `${prefixSelector.trimRight()}${slottedSelector.trim()}`;
  189. if (orgSelector.trim() !== addedSelector.trim()) {
  190. const updatedSelector = `${addedSelector}, ${orgSelector}`;
  191. selectors.push({
  192. orgSelector,
  193. updatedSelector,
  194. });
  195. }
  196. return slottedSelector;
  197. }
  198. else {
  199. return _polyfillHostNoCombinator + m[3];
  200. }
  201. });
  202. return {
  203. selectors,
  204. cssText,
  205. };
  206. };
  207. const convertColonHostContext = (cssText) => {
  208. return convertColonRule(cssText, _cssColonHostContextRe, colonHostContextPartReplacer);
  209. };
  210. const convertShadowDOMSelectors = (cssText) => {
  211. return _shadowDOMSelectorsRe.reduce((result, pattern) => result.replace(pattern, ' '), cssText);
  212. };
  213. const makeScopeMatcher = (scopeSelector) => {
  214. const lre = /\[/g;
  215. const rre = /\]/g;
  216. scopeSelector = scopeSelector.replace(lre, '\\[').replace(rre, '\\]');
  217. return new RegExp('^(' + scopeSelector + ')' + _selectorReSuffix, 'm');
  218. };
  219. const selectorNeedsScoping = (selector, scopeSelector) => {
  220. const re = makeScopeMatcher(scopeSelector);
  221. return !re.test(selector);
  222. };
  223. const injectScopingSelector = (selector, scopingSelector) => {
  224. return selector.replace(_selectorPartsRe, (_, before = '', _colonGroup, colon = '', after = '') => {
  225. return before + scopingSelector + colon + after;
  226. });
  227. };
  228. const applySimpleSelectorScope = (selector, scopeSelector, hostSelector) => {
  229. // In Android browser, the lastIndex is not reset when the regex is used in String.replace()
  230. _polyfillHostRe.lastIndex = 0;
  231. if (_polyfillHostRe.test(selector)) {
  232. const replaceBy = `.${hostSelector}`;
  233. return selector
  234. .replace(_polyfillHostNoCombinatorRe, (_, selector) => injectScopingSelector(selector, replaceBy))
  235. .replace(_polyfillHostRe, replaceBy + ' ');
  236. }
  237. return scopeSelector + ' ' + selector;
  238. };
  239. const 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. scopedP = injectScopingSelector(t, className);
  256. }
  257. }
  258. return scopedP;
  259. };
  260. const safeContent = safeSelector(selector);
  261. selector = safeContent.content;
  262. let scopedSelector = '';
  263. let startIndex = 0;
  264. let res;
  265. const sep = /( |>|\+|~(?!=))\s*/g;
  266. // If a selector appears before :host it should not be shimmed as it
  267. // matches on ancestor elements and not on elements in the host's shadow
  268. // `:host-context(div)` is transformed to
  269. // `-shadowcsshost-no-combinatordiv, div -shadowcsshost-no-combinator`
  270. // the `div` is not part of the component in the 2nd selectors and should not be scoped.
  271. // Historically `component-tag:host` was matching the component so we also want to preserve
  272. // this behavior to avoid breaking legacy apps (it should not match).
  273. // The behavior should be:
  274. // - `tag:host` -> `tag[h]` (this is to avoid breaking legacy apps, should not match anything)
  275. // - `tag :host` -> `tag [h]` (`tag` is not scoped because it's considered part of a
  276. // `:host-context(tag)`)
  277. const hasHost = selector.indexOf(_polyfillHostNoCombinator) > -1;
  278. // Only scope parts after the first `-shadowcsshost-no-combinator` when it is present
  279. let shouldScope = !hasHost;
  280. while ((res = sep.exec(selector)) !== null) {
  281. const separator = res[1];
  282. const part = selector.slice(startIndex, res.index).trim();
  283. shouldScope = shouldScope || part.indexOf(_polyfillHostNoCombinator) > -1;
  284. const scopedPart = shouldScope ? _scopeSelectorPart(part) : part;
  285. scopedSelector += `${scopedPart} ${separator} `;
  286. startIndex = sep.lastIndex;
  287. }
  288. const part = selector.substring(startIndex);
  289. shouldScope = shouldScope || part.indexOf(_polyfillHostNoCombinator) > -1;
  290. scopedSelector += shouldScope ? _scopeSelectorPart(part) : part;
  291. // replace the placeholders with their original values
  292. return restoreSafeSelector(safeContent.placeholders, scopedSelector);
  293. };
  294. const scopeSelector = (selector, scopeSelectorText, hostSelector, slotSelector) => {
  295. return selector
  296. .split(',')
  297. .map((shallowPart) => {
  298. if (slotSelector && shallowPart.indexOf('.' + slotSelector) > -1) {
  299. return shallowPart.trim();
  300. }
  301. if (selectorNeedsScoping(shallowPart, scopeSelectorText)) {
  302. return applyStrictSelectorScope(shallowPart, scopeSelectorText, hostSelector).trim();
  303. }
  304. else {
  305. return shallowPart.trim();
  306. }
  307. })
  308. .join(', ');
  309. };
  310. const scopeSelectors = (cssText, scopeSelectorText, hostSelector, slotSelector, commentOriginalSelector) => {
  311. return processRules(cssText, (rule) => {
  312. let selector = rule.selector;
  313. let content = rule.content;
  314. if (rule.selector[0] !== '@') {
  315. selector = scopeSelector(rule.selector, scopeSelectorText, hostSelector, slotSelector);
  316. }
  317. else if (rule.selector.startsWith('@media') ||
  318. rule.selector.startsWith('@supports') ||
  319. rule.selector.startsWith('@page') ||
  320. 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. };
  330. const 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. };
  347. const 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') ||
  366. rule.selector.startsWith('@supports') ||
  367. rule.selector.startsWith('@page') ||
  368. rule.selector.startsWith('@document')) {
  369. rule.content = processRules(rule.content, processCommentedSelector);
  370. return rule;
  371. }
  372. return rule;
  373. });
  374. }
  375. const scoped = scopeCssText(cssText, scopeId, hostScopeId, slotScopeId);
  376. cssText = [scoped.cssText, ...commentsWithHash].join('\n');
  377. if (commentOriginalSelector) {
  378. orgSelectors.forEach(({ placeholder, comment }) => {
  379. cssText = cssText.replace(placeholder, comment);
  380. });
  381. }
  382. scoped.slottedSelectors.forEach((slottedSelector) => {
  383. cssText = cssText.replace(slottedSelector.orgSelector, slottedSelector.updatedSelector);
  384. });
  385. return cssText;
  386. };
  387. export { scopeCss };