source: frontend/node_modules/postcss-modules-local-by-default/src/index.js

Last change on this file was 9af201e, checked in by MBK <marija.karapandzova@…>, 12 days ago

Fix frontend appearance

  • Property mode set to 100644
File size: 20.4 KB
RevLine 
[9af201e]1"use strict";
2
3const selectorParser = require("postcss-selector-parser");
4const valueParser = require("postcss-value-parser");
5const { extractICSS } = require("icss-utils");
6
7const IGNORE_FILE_MARKER = "cssmodules-pure-no-check";
8const IGNORE_NEXT_LINE_MARKER = "cssmodules-pure-ignore";
9
10const isSpacing = (node) => node.type === "combinator" && node.value === " ";
11
12const isPureCheckDisabled = (root) => {
13 for (const node of root.nodes) {
14 if (node.type !== "comment") {
15 return false;
16 }
17 if (node.text.trim().startsWith(IGNORE_FILE_MARKER)) {
18 return true;
19 }
20 }
21 return false;
22};
23
24function getIgnoreComment(node) {
25 if (!node.parent) {
26 return;
27 }
28 const indexInParent = node.parent.index(node);
29 for (let i = indexInParent - 1; i >= 0; i--) {
30 const prevNode = node.parent.nodes[i];
31 if (prevNode.type === "comment") {
32 if (prevNode.text.trimStart().startsWith(IGNORE_NEXT_LINE_MARKER)) {
33 return prevNode;
34 }
35 } else {
36 break;
37 }
38 }
39}
40
41function normalizeNodeArray(nodes) {
42 const array = [];
43
44 nodes.forEach((x) => {
45 if (Array.isArray(x)) {
46 normalizeNodeArray(x).forEach((item) => {
47 array.push(item);
48 });
49 } else if (x) {
50 array.push(x);
51 }
52 });
53
54 if (array.length > 0 && isSpacing(array[array.length - 1])) {
55 array.pop();
56 }
57 return array;
58}
59
60const isPureSelectorSymbol = Symbol("is-pure-selector");
61
62function localizeNode(rule, mode, localAliasMap) {
63 const transform = (node, context) => {
64 if (context.ignoreNextSpacing && !isSpacing(node)) {
65 throw new Error("Missing whitespace after " + context.ignoreNextSpacing);
66 }
67
68 if (context.enforceNoSpacing && isSpacing(node)) {
69 throw new Error("Missing whitespace before " + context.enforceNoSpacing);
70 }
71
72 let newNodes;
73
74 switch (node.type) {
75 case "root": {
76 let resultingGlobal;
77
78 context.hasPureGlobals = false;
79
80 newNodes = node.nodes.map((n) => {
81 const nContext = {
82 global: context.global,
83 lastWasSpacing: true,
84 hasLocals: false,
85 explicit: false,
86 };
87
88 n = transform(n, nContext);
89
90 if (typeof resultingGlobal === "undefined") {
91 resultingGlobal = nContext.global;
92 } else if (resultingGlobal !== nContext.global) {
93 throw new Error(
94 'Inconsistent rule global/local result in rule "' +
95 node +
96 '" (multiple selectors must result in the same mode for the rule)'
97 );
98 }
99
100 if (!nContext.hasLocals) {
101 context.hasPureGlobals = true;
102 }
103
104 return n;
105 });
106
107 context.global = resultingGlobal;
108
109 node.nodes = normalizeNodeArray(newNodes);
110 break;
111 }
112 case "selector": {
113 newNodes = node.map((childNode) => transform(childNode, context));
114
115 node = node.clone();
116 node.nodes = normalizeNodeArray(newNodes);
117 break;
118 }
119 case "combinator": {
120 if (isSpacing(node)) {
121 if (context.ignoreNextSpacing) {
122 context.ignoreNextSpacing = false;
123 context.lastWasSpacing = false;
124 context.enforceNoSpacing = false;
125 return null;
126 }
127 context.lastWasSpacing = true;
128 return node;
129 }
130 break;
131 }
132 case "pseudo": {
133 let childContext;
134 const isNested = !!node.length;
135 const isScoped = node.value === ":local" || node.value === ":global";
136 const isImportExport =
137 node.value === ":import" || node.value === ":export";
138
139 if (isImportExport) {
140 context.hasLocals = true;
141 // :local(.foo)
142 } else if (isNested) {
143 if (isScoped) {
144 if (node.nodes.length === 0) {
145 throw new Error(`${node.value}() can't be empty`);
146 }
147
148 if (context.inside) {
149 throw new Error(
150 `A ${node.value} is not allowed inside of a ${context.inside}(...)`
151 );
152 }
153
154 childContext = {
155 global: node.value === ":global",
156 inside: node.value,
157 hasLocals: false,
158 explicit: true,
159 };
160
161 newNodes = node
162 .map((childNode) => transform(childNode, childContext))
163 .reduce((acc, next) => acc.concat(next.nodes), []);
164
165 if (newNodes.length) {
166 const { before, after } = node.spaces;
167
168 const first = newNodes[0];
169 const last = newNodes[newNodes.length - 1];
170
171 first.spaces = { before, after: first.spaces.after };
172 last.spaces = { before: last.spaces.before, after };
173 }
174
175 node = newNodes;
176
177 break;
178 } else {
179 childContext = {
180 global: context.global,
181 inside: context.inside,
182 lastWasSpacing: true,
183 hasLocals: false,
184 explicit: context.explicit,
185 };
186 newNodes = node.map((childNode) => {
187 const newContext = {
188 ...childContext,
189 enforceNoSpacing: false,
190 };
191
192 const result = transform(childNode, newContext);
193
194 childContext.global = newContext.global;
195 childContext.hasLocals = newContext.hasLocals;
196
197 return result;
198 });
199
200 node = node.clone();
201 node.nodes = normalizeNodeArray(newNodes);
202
203 if (childContext.hasLocals) {
204 context.hasLocals = true;
205 }
206 }
207 break;
208
209 //:local .foo .bar
210 } else if (isScoped) {
211 if (context.inside) {
212 throw new Error(
213 `A ${node.value} is not allowed inside of a ${context.inside}(...)`
214 );
215 }
216
217 const addBackSpacing = !!node.spaces.before;
218
219 context.ignoreNextSpacing = context.lastWasSpacing
220 ? node.value
221 : false;
222
223 context.enforceNoSpacing = context.lastWasSpacing
224 ? false
225 : node.value;
226
227 context.global = node.value === ":global";
228 context.explicit = true;
229
230 // because this node has spacing that is lost when we remove it
231 // we make up for it by adding an extra combinator in since adding
232 // spacing on the parent selector doesn't work
233 return addBackSpacing
234 ? selectorParser.combinator({ value: " " })
235 : null;
236 }
237 break;
238 }
239 case "id":
240 case "class": {
241 if (!node.value) {
242 throw new Error("Invalid class or id selector syntax");
243 }
244
245 if (context.global) {
246 break;
247 }
248
249 const isImportedValue = localAliasMap.has(node.value);
250 const isImportedWithExplicitScope = isImportedValue && context.explicit;
251
252 if (!isImportedValue || isImportedWithExplicitScope) {
253 const innerNode = node.clone();
254 innerNode.spaces = { before: "", after: "" };
255
256 node = selectorParser.pseudo({
257 value: ":local",
258 nodes: [innerNode],
259 spaces: node.spaces,
260 });
261
262 context.hasLocals = true;
263 }
264
265 break;
266 }
267 case "nesting": {
268 if (node.value === "&") {
269 context.hasLocals = rule.parent[isPureSelectorSymbol];
270 }
271 }
272 }
273
274 context.lastWasSpacing = false;
275 context.ignoreNextSpacing = false;
276 context.enforceNoSpacing = false;
277
278 return node;
279 };
280
281 const rootContext = {
282 global: mode === "global",
283 hasPureGlobals: false,
284 };
285
286 rootContext.selector = selectorParser((root) => {
287 transform(root, rootContext);
288 }).processSync(rule, { updateSelector: false, lossless: true });
289
290 return rootContext;
291}
292
293function localizeDeclNode(node, context) {
294 switch (node.type) {
295 case "word":
296 if (context.localizeNextItem) {
297 if (!context.localAliasMap.has(node.value)) {
298 node.value = ":local(" + node.value + ")";
299 context.localizeNextItem = false;
300 }
301 }
302 break;
303
304 case "function":
305 if (
306 context.options &&
307 context.options.rewriteUrl &&
308 node.value.toLowerCase() === "url"
309 ) {
310 node.nodes.map((nestedNode) => {
311 if (nestedNode.type !== "string" && nestedNode.type !== "word") {
312 return;
313 }
314
315 let newUrl = context.options.rewriteUrl(
316 context.global,
317 nestedNode.value
318 );
319
320 switch (nestedNode.type) {
321 case "string":
322 if (nestedNode.quote === "'") {
323 newUrl = newUrl.replace(/(\\)/g, "\\$1").replace(/'/g, "\\'");
324 }
325
326 if (nestedNode.quote === '"') {
327 newUrl = newUrl.replace(/(\\)/g, "\\$1").replace(/"/g, '\\"');
328 }
329
330 break;
331 case "word":
332 newUrl = newUrl.replace(/("|'|\)|\\)/g, "\\$1");
333 break;
334 }
335
336 nestedNode.value = newUrl;
337 });
338 }
339 break;
340 }
341 return node;
342}
343
344// `none` is special value, other is global values
345const specialKeywords = [
346 "none",
347 "inherit",
348 "initial",
349 "revert",
350 "revert-layer",
351 "unset",
352];
353
354function localizeDeclarationValues(localize, declaration, context) {
355 const valueNodes = valueParser(declaration.value);
356
357 valueNodes.walk((node, index, nodes) => {
358 if (
359 node.type === "function" &&
360 (node.value.toLowerCase() === "var" || node.value.toLowerCase() === "env")
361 ) {
362 return false;
363 }
364
365 if (
366 node.type === "word" &&
367 specialKeywords.includes(node.value.toLowerCase())
368 ) {
369 return;
370 }
371
372 const subContext = {
373 options: context.options,
374 global: context.global,
375 localizeNextItem: localize && !context.global,
376 localAliasMap: context.localAliasMap,
377 };
378 nodes[index] = localizeDeclNode(node, subContext);
379 });
380
381 declaration.value = valueNodes.toString();
382}
383
384// letter
385// An uppercase letter or a lowercase letter.
386//
387// ident-start code point
388// A letter, a non-ASCII code point, or U+005F LOW LINE (_).
389//
390// ident code point
391// An ident-start code point, a digit, or U+002D HYPHEN-MINUS (-).
392
393// We don't validate `hex digits`, because we don't need it, it is work of linters.
394const validIdent =
395 /^-?([a-z\u0080-\uFFFF_]|(\\[^\r\n\f])|-(?![0-9]))((\\[^\r\n\f])|[a-z\u0080-\uFFFF_0-9-])*$/i;
396
397/*
398 The spec defines some keywords that you can use to describe properties such as the timing
399 function. These are still valid animation names, so as long as there is a property that accepts
400 a keyword, it is given priority. Only when all the properties that can take a keyword are
401 exhausted can the animation name be set to the keyword. I.e.
402
403 animation: infinite infinite;
404
405 The animation will repeat an infinite number of times from the first argument, and will have an
406 animation name of infinite from the second.
407 */
408const animationKeywords = {
409 // animation-direction
410 $normal: 1,
411 $reverse: 1,
412 $alternate: 1,
413 "$alternate-reverse": 1,
414 // animation-fill-mode
415 $forwards: 1,
416 $backwards: 1,
417 $both: 1,
418 // animation-iteration-count
419 $infinite: 1,
420 // animation-play-state
421 $paused: 1,
422 $running: 1,
423 // animation-timing-function
424 $ease: 1,
425 "$ease-in": 1,
426 "$ease-out": 1,
427 "$ease-in-out": 1,
428 $linear: 1,
429 "$step-end": 1,
430 "$step-start": 1,
431 // Special
432 $none: Infinity, // No matter how many times you write none, it will never be an animation name
433 // Global values
434 $initial: Infinity,
435 $inherit: Infinity,
436 $unset: Infinity,
437 $revert: Infinity,
438 "$revert-layer": Infinity,
439};
440
441function localizeDeclaration(declaration, context) {
442 const isAnimation = /animation(-name)?$/i.test(declaration.prop);
443
444 if (isAnimation) {
445 let parsedAnimationKeywords = {};
446 const valueNodes = valueParser(declaration.value).walk((node) => {
447 // If div-token appeared (represents as comma ','), a possibility of an animation-keywords should be reflesh.
448 if (node.type === "div") {
449 parsedAnimationKeywords = {};
450
451 return;
452 } else if (
453 node.type === "function" &&
454 node.value.toLowerCase() === "local" &&
455 node.nodes.length === 1
456 ) {
457 node.type = "word";
458 node.value = node.nodes[0].value;
459
460 return localizeDeclNode(node, {
461 options: context.options,
462 global: context.global,
463 localizeNextItem: true,
464 localAliasMap: context.localAliasMap,
465 });
466 } else if (node.type === "function") {
467 // replace `animation: global(example)` with `animation-name: example`
468 if (node.value.toLowerCase() === "global" && node.nodes.length === 1) {
469 node.type = "word";
470 node.value = node.nodes[0].value;
471 }
472
473 // Do not handle nested functions
474 return false;
475 }
476 // Ignore all except word
477 else if (node.type !== "word") {
478 return;
479 }
480
481 const value = node.type === "word" ? node.value.toLowerCase() : null;
482
483 let shouldParseAnimationName = false;
484
485 if (value && validIdent.test(value)) {
486 if ("$" + value in animationKeywords) {
487 parsedAnimationKeywords["$" + value] =
488 "$" + value in parsedAnimationKeywords
489 ? parsedAnimationKeywords["$" + value] + 1
490 : 0;
491
492 shouldParseAnimationName =
493 parsedAnimationKeywords["$" + value] >=
494 animationKeywords["$" + value];
495 } else {
496 shouldParseAnimationName = true;
497 }
498 }
499
500 return localizeDeclNode(node, {
501 options: context.options,
502 global: context.global,
503 localizeNextItem: shouldParseAnimationName && !context.global,
504 localAliasMap: context.localAliasMap,
505 });
506 });
507
508 declaration.value = valueNodes.toString();
509
510 return;
511 }
512
513 if (/url\(/i.test(declaration.value)) {
514 return localizeDeclarationValues(false, declaration, context);
515 }
516}
517
518const isPureSelector = (context, rule) => {
519 if (!rule.parent || rule.type === "root") {
520 return !context.hasPureGlobals;
521 }
522
523 if (rule.type === "rule" && rule[isPureSelectorSymbol]) {
524 return rule[isPureSelectorSymbol] || isPureSelector(context, rule.parent);
525 }
526
527 return !context.hasPureGlobals || isPureSelector(context, rule.parent);
528};
529
530const isNodeWithoutDeclarations = (rule) => {
531 if (rule.nodes.length > 0) {
532 return !rule.nodes.every(
533 (item) =>
534 item.type === "rule" ||
535 (item.type === "atrule" && !isNodeWithoutDeclarations(item))
536 );
537 }
538
539 return true;
540};
541
542module.exports = (options = {}) => {
543 if (
544 options &&
545 options.mode &&
546 options.mode !== "global" &&
547 options.mode !== "local" &&
548 options.mode !== "pure"
549 ) {
550 throw new Error(
551 'options.mode must be either "global", "local" or "pure" (default "local")'
552 );
553 }
554
555 const pureMode = options && options.mode === "pure";
556 const globalMode = options && options.mode === "global";
557
558 return {
559 postcssPlugin: "postcss-modules-local-by-default",
560 prepare() {
561 const localAliasMap = new Map();
562
563 return {
564 Once(root) {
565 const { icssImports } = extractICSS(root, false);
566 const enforcePureMode = pureMode && !isPureCheckDisabled(root);
567
568 Object.keys(icssImports).forEach((key) => {
569 Object.keys(icssImports[key]).forEach((prop) => {
570 localAliasMap.set(prop, icssImports[key][prop]);
571 });
572 });
573
574 root.walkAtRules((atRule) => {
575 if (/keyframes$/i.test(atRule.name)) {
576 const globalMatch = /^\s*:global\s*\((.+)\)\s*$/.exec(
577 atRule.params
578 );
579 const localMatch = /^\s*:local\s*\((.+)\)\s*$/.exec(
580 atRule.params
581 );
582
583 let globalKeyframes = globalMode;
584
585 if (globalMatch) {
586 if (enforcePureMode) {
587 const ignoreComment = getIgnoreComment(atRule);
588 if (!ignoreComment) {
589 throw atRule.error(
590 "@keyframes :global(...) is not allowed in pure mode"
591 );
592 } else {
593 ignoreComment.remove();
594 }
595 }
596 atRule.params = globalMatch[1];
597 globalKeyframes = true;
598 } else if (localMatch) {
599 atRule.params = localMatch[0];
600 globalKeyframes = false;
601 } else if (
602 atRule.params &&
603 !globalMode &&
604 !localAliasMap.has(atRule.params)
605 ) {
606 atRule.params = ":local(" + atRule.params + ")";
607 }
608
609 atRule.walkDecls((declaration) => {
610 localizeDeclaration(declaration, {
611 localAliasMap,
612 options: options,
613 global: globalKeyframes,
614 });
615 });
616 } else if (/scope$/i.test(atRule.name)) {
617 if (atRule.params) {
618 const ignoreComment = pureMode
619 ? getIgnoreComment(atRule)
620 : undefined;
621
622 if (ignoreComment) {
623 ignoreComment.remove();
624 }
625
626 atRule.params = atRule.params
627 .split("to")
628 .map((item) => {
629 const selector = item.trim().slice(1, -1).trim();
630 const context = localizeNode(
631 selector,
632 options.mode,
633 localAliasMap
634 );
635
636 context.options = options;
637 context.localAliasMap = localAliasMap;
638
639 if (
640 enforcePureMode &&
641 context.hasPureGlobals &&
642 !ignoreComment
643 ) {
644 throw atRule.error(
645 'Selector in at-rule"' +
646 selector +
647 '" is not pure ' +
648 "(pure selectors must contain at least one local class or id)"
649 );
650 }
651
652 return `(${context.selector})`;
653 })
654 .join(" to ");
655 }
656
657 atRule.nodes.forEach((declaration) => {
658 if (declaration.type === "decl") {
659 localizeDeclaration(declaration, {
660 localAliasMap,
661 options: options,
662 global: globalMode,
663 });
664 }
665 });
666 } else if (atRule.nodes) {
667 atRule.nodes.forEach((declaration) => {
668 if (declaration.type === "decl") {
669 localizeDeclaration(declaration, {
670 localAliasMap,
671 options: options,
672 global: globalMode,
673 });
674 }
675 });
676 }
677 });
678
679 root.walkRules((rule) => {
680 if (
681 rule.parent &&
682 rule.parent.type === "atrule" &&
683 /keyframes$/i.test(rule.parent.name)
684 ) {
685 // ignore keyframe rules
686 return;
687 }
688
689 const context = localizeNode(rule, options.mode, localAliasMap);
690
691 context.options = options;
692 context.localAliasMap = localAliasMap;
693
694 const ignoreComment = enforcePureMode
695 ? getIgnoreComment(rule)
696 : undefined;
697 const isNotPure = enforcePureMode && !isPureSelector(context, rule);
698
699 if (
700 isNotPure &&
701 isNodeWithoutDeclarations(rule) &&
702 !ignoreComment
703 ) {
704 throw rule.error(
705 'Selector "' +
706 rule.selector +
707 '" is not pure ' +
708 "(pure selectors must contain at least one local class or id)"
709 );
710 } else if (ignoreComment) {
711 ignoreComment.remove();
712 }
713
714 if (pureMode) {
715 rule[isPureSelectorSymbol] = !isNotPure;
716 }
717
718 rule.selector = context.selector;
719
720 // Less-syntax mixins parse as rules with no nodes
721 if (rule.nodes) {
722 rule.nodes.forEach((declaration) =>
723 localizeDeclaration(declaration, context)
724 );
725 }
726 });
727 },
728 };
729 },
730 };
731};
732module.exports.postcss = true;
Note: See TracBrowser for help on using the repository browser.