source: trip-planner-front/node_modules/postcss-modules-local-by-default/src/index.js@ 8d391a1

Last change on this file since 8d391a1 was 6a3a178, checked in by Ema <ema_spirova@…>, 3 years ago

initial commit

  • Property mode set to 100644
File size: 15.1 KB
Line 
1"use strict";
2
3const selectorParser = require("postcss-selector-parser");
4const valueParser = require("postcss-value-parser");
5const { extractICSS } = require("icss-utils");
6
7const isSpacing = (node) => node.type === "combinator" && node.value === " ";
8
9function normalizeNodeArray(nodes) {
10 const array = [];
11
12 nodes.forEach((x) => {
13 if (Array.isArray(x)) {
14 normalizeNodeArray(x).forEach((item) => {
15 array.push(item);
16 });
17 } else if (x) {
18 array.push(x);
19 }
20 });
21
22 if (array.length > 0 && isSpacing(array[array.length - 1])) {
23 array.pop();
24 }
25 return array;
26}
27
28function localizeNode(rule, mode, localAliasMap) {
29 const transform = (node, context) => {
30 if (context.ignoreNextSpacing && !isSpacing(node)) {
31 throw new Error("Missing whitespace after " + context.ignoreNextSpacing);
32 }
33
34 if (context.enforceNoSpacing && isSpacing(node)) {
35 throw new Error("Missing whitespace before " + context.enforceNoSpacing);
36 }
37
38 let newNodes;
39
40 switch (node.type) {
41 case "root": {
42 let resultingGlobal;
43
44 context.hasPureGlobals = false;
45
46 newNodes = node.nodes.map((n) => {
47 const nContext = {
48 global: context.global,
49 lastWasSpacing: true,
50 hasLocals: false,
51 explicit: false,
52 };
53
54 n = transform(n, nContext);
55
56 if (typeof resultingGlobal === "undefined") {
57 resultingGlobal = nContext.global;
58 } else if (resultingGlobal !== nContext.global) {
59 throw new Error(
60 'Inconsistent rule global/local result in rule "' +
61 node +
62 '" (multiple selectors must result in the same mode for the rule)'
63 );
64 }
65
66 if (!nContext.hasLocals) {
67 context.hasPureGlobals = true;
68 }
69
70 return n;
71 });
72
73 context.global = resultingGlobal;
74
75 node.nodes = normalizeNodeArray(newNodes);
76 break;
77 }
78 case "selector": {
79 newNodes = node.map((childNode) => transform(childNode, context));
80
81 node = node.clone();
82 node.nodes = normalizeNodeArray(newNodes);
83 break;
84 }
85 case "combinator": {
86 if (isSpacing(node)) {
87 if (context.ignoreNextSpacing) {
88 context.ignoreNextSpacing = false;
89 context.lastWasSpacing = false;
90 context.enforceNoSpacing = false;
91 return null;
92 }
93 context.lastWasSpacing = true;
94 return node;
95 }
96 break;
97 }
98 case "pseudo": {
99 let childContext;
100 const isNested = !!node.length;
101 const isScoped = node.value === ":local" || node.value === ":global";
102 const isImportExport =
103 node.value === ":import" || node.value === ":export";
104
105 if (isImportExport) {
106 context.hasLocals = true;
107 // :local(.foo)
108 } else if (isNested) {
109 if (isScoped) {
110 if (node.nodes.length === 0) {
111 throw new Error(`${node.value}() can't be empty`);
112 }
113
114 if (context.inside) {
115 throw new Error(
116 `A ${node.value} is not allowed inside of a ${context.inside}(...)`
117 );
118 }
119
120 childContext = {
121 global: node.value === ":global",
122 inside: node.value,
123 hasLocals: false,
124 explicit: true,
125 };
126
127 newNodes = node
128 .map((childNode) => transform(childNode, childContext))
129 .reduce((acc, next) => acc.concat(next.nodes), []);
130
131 if (newNodes.length) {
132 const { before, after } = node.spaces;
133
134 const first = newNodes[0];
135 const last = newNodes[newNodes.length - 1];
136
137 first.spaces = { before, after: first.spaces.after };
138 last.spaces = { before: last.spaces.before, after };
139 }
140
141 node = newNodes;
142
143 break;
144 } else {
145 childContext = {
146 global: context.global,
147 inside: context.inside,
148 lastWasSpacing: true,
149 hasLocals: false,
150 explicit: context.explicit,
151 };
152 newNodes = node.map((childNode) =>
153 transform(childNode, childContext)
154 );
155
156 node = node.clone();
157 node.nodes = normalizeNodeArray(newNodes);
158
159 if (childContext.hasLocals) {
160 context.hasLocals = true;
161 }
162 }
163 break;
164
165 //:local .foo .bar
166 } else if (isScoped) {
167 if (context.inside) {
168 throw new Error(
169 `A ${node.value} is not allowed inside of a ${context.inside}(...)`
170 );
171 }
172
173 const addBackSpacing = !!node.spaces.before;
174
175 context.ignoreNextSpacing = context.lastWasSpacing
176 ? node.value
177 : false;
178
179 context.enforceNoSpacing = context.lastWasSpacing
180 ? false
181 : node.value;
182
183 context.global = node.value === ":global";
184 context.explicit = true;
185
186 // because this node has spacing that is lost when we remove it
187 // we make up for it by adding an extra combinator in since adding
188 // spacing on the parent selector doesn't work
189 return addBackSpacing
190 ? selectorParser.combinator({ value: " " })
191 : null;
192 }
193 break;
194 }
195 case "id":
196 case "class": {
197 if (!node.value) {
198 throw new Error("Invalid class or id selector syntax");
199 }
200
201 if (context.global) {
202 break;
203 }
204
205 const isImportedValue = localAliasMap.has(node.value);
206 const isImportedWithExplicitScope = isImportedValue && context.explicit;
207
208 if (!isImportedValue || isImportedWithExplicitScope) {
209 const innerNode = node.clone();
210 innerNode.spaces = { before: "", after: "" };
211
212 node = selectorParser.pseudo({
213 value: ":local",
214 nodes: [innerNode],
215 spaces: node.spaces,
216 });
217
218 context.hasLocals = true;
219 }
220
221 break;
222 }
223 }
224
225 context.lastWasSpacing = false;
226 context.ignoreNextSpacing = false;
227 context.enforceNoSpacing = false;
228
229 return node;
230 };
231
232 const rootContext = {
233 global: mode === "global",
234 hasPureGlobals: false,
235 };
236
237 rootContext.selector = selectorParser((root) => {
238 transform(root, rootContext);
239 }).processSync(rule, { updateSelector: false, lossless: true });
240
241 return rootContext;
242}
243
244function localizeDeclNode(node, context) {
245 switch (node.type) {
246 case "word":
247 if (context.localizeNextItem) {
248 if (!context.localAliasMap.has(node.value)) {
249 node.value = ":local(" + node.value + ")";
250 context.localizeNextItem = false;
251 }
252 }
253 break;
254
255 case "function":
256 if (
257 context.options &&
258 context.options.rewriteUrl &&
259 node.value.toLowerCase() === "url"
260 ) {
261 node.nodes.map((nestedNode) => {
262 if (nestedNode.type !== "string" && nestedNode.type !== "word") {
263 return;
264 }
265
266 let newUrl = context.options.rewriteUrl(
267 context.global,
268 nestedNode.value
269 );
270
271 switch (nestedNode.type) {
272 case "string":
273 if (nestedNode.quote === "'") {
274 newUrl = newUrl.replace(/(\\)/g, "\\$1").replace(/'/g, "\\'");
275 }
276
277 if (nestedNode.quote === '"') {
278 newUrl = newUrl.replace(/(\\)/g, "\\$1").replace(/"/g, '\\"');
279 }
280
281 break;
282 case "word":
283 newUrl = newUrl.replace(/("|'|\)|\\)/g, "\\$1");
284 break;
285 }
286
287 nestedNode.value = newUrl;
288 });
289 }
290 break;
291 }
292 return node;
293}
294
295function isWordAFunctionArgument(wordNode, functionNode) {
296 return functionNode
297 ? functionNode.nodes.some(
298 (functionNodeChild) =>
299 functionNodeChild.sourceIndex === wordNode.sourceIndex
300 )
301 : false;
302}
303
304function localizeDeclarationValues(localize, declaration, context) {
305 const valueNodes = valueParser(declaration.value);
306
307 valueNodes.walk((node, index, nodes) => {
308 const subContext = {
309 options: context.options,
310 global: context.global,
311 localizeNextItem: localize && !context.global,
312 localAliasMap: context.localAliasMap,
313 };
314 nodes[index] = localizeDeclNode(node, subContext);
315 });
316
317 declaration.value = valueNodes.toString();
318}
319
320function localizeDeclaration(declaration, context) {
321 const isAnimation = /animation$/i.test(declaration.prop);
322
323 if (isAnimation) {
324 const validIdent = /^-?[_a-z][_a-z0-9-]*$/i;
325
326 /*
327 The spec defines some keywords that you can use to describe properties such as the timing
328 function. These are still valid animation names, so as long as there is a property that accepts
329 a keyword, it is given priority. Only when all the properties that can take a keyword are
330 exhausted can the animation name be set to the keyword. I.e.
331
332 animation: infinite infinite;
333
334 The animation will repeat an infinite number of times from the first argument, and will have an
335 animation name of infinite from the second.
336 */
337 const animationKeywords = {
338 $alternate: 1,
339 "$alternate-reverse": 1,
340 $backwards: 1,
341 $both: 1,
342 $ease: 1,
343 "$ease-in": 1,
344 "$ease-in-out": 1,
345 "$ease-out": 1,
346 $forwards: 1,
347 $infinite: 1,
348 $linear: 1,
349 $none: Infinity, // No matter how many times you write none, it will never be an animation name
350 $normal: 1,
351 $paused: 1,
352 $reverse: 1,
353 $running: 1,
354 "$step-end": 1,
355 "$step-start": 1,
356 $initial: Infinity,
357 $inherit: Infinity,
358 $unset: Infinity,
359 };
360
361 const didParseAnimationName = false;
362 let parsedAnimationKeywords = {};
363 let stepsFunctionNode = null;
364 const valueNodes = valueParser(declaration.value).walk((node) => {
365 /* If div-token appeared (represents as comma ','), a possibility of an animation-keywords should be reflesh. */
366 if (node.type === "div") {
367 parsedAnimationKeywords = {};
368 }
369 if (node.type === "function" && node.value.toLowerCase() === "steps") {
370 stepsFunctionNode = node;
371 }
372 const value =
373 node.type === "word" &&
374 !isWordAFunctionArgument(node, stepsFunctionNode)
375 ? node.value.toLowerCase()
376 : null;
377
378 let shouldParseAnimationName = false;
379
380 if (!didParseAnimationName && value && validIdent.test(value)) {
381 if ("$" + value in animationKeywords) {
382 parsedAnimationKeywords["$" + value] =
383 "$" + value in parsedAnimationKeywords
384 ? parsedAnimationKeywords["$" + value] + 1
385 : 0;
386
387 shouldParseAnimationName =
388 parsedAnimationKeywords["$" + value] >=
389 animationKeywords["$" + value];
390 } else {
391 shouldParseAnimationName = true;
392 }
393 }
394
395 const subContext = {
396 options: context.options,
397 global: context.global,
398 localizeNextItem: shouldParseAnimationName && !context.global,
399 localAliasMap: context.localAliasMap,
400 };
401 return localizeDeclNode(node, subContext);
402 });
403
404 declaration.value = valueNodes.toString();
405
406 return;
407 }
408
409 const isAnimationName = /animation(-name)?$/i.test(declaration.prop);
410
411 if (isAnimationName) {
412 return localizeDeclarationValues(true, declaration, context);
413 }
414
415 const hasUrl = /url\(/i.test(declaration.value);
416
417 if (hasUrl) {
418 return localizeDeclarationValues(false, declaration, context);
419 }
420}
421
422module.exports = (options = {}) => {
423 if (
424 options &&
425 options.mode &&
426 options.mode !== "global" &&
427 options.mode !== "local" &&
428 options.mode !== "pure"
429 ) {
430 throw new Error(
431 'options.mode must be either "global", "local" or "pure" (default "local")'
432 );
433 }
434
435 const pureMode = options && options.mode === "pure";
436 const globalMode = options && options.mode === "global";
437
438 return {
439 postcssPlugin: "postcss-modules-local-by-default",
440 prepare() {
441 const localAliasMap = new Map();
442
443 return {
444 Once(root) {
445 const { icssImports } = extractICSS(root, false);
446
447 Object.keys(icssImports).forEach((key) => {
448 Object.keys(icssImports[key]).forEach((prop) => {
449 localAliasMap.set(prop, icssImports[key][prop]);
450 });
451 });
452
453 root.walkAtRules((atRule) => {
454 if (/keyframes$/i.test(atRule.name)) {
455 const globalMatch = /^\s*:global\s*\((.+)\)\s*$/.exec(
456 atRule.params
457 );
458 const localMatch = /^\s*:local\s*\((.+)\)\s*$/.exec(
459 atRule.params
460 );
461
462 let globalKeyframes = globalMode;
463
464 if (globalMatch) {
465 if (pureMode) {
466 throw atRule.error(
467 "@keyframes :global(...) is not allowed in pure mode"
468 );
469 }
470 atRule.params = globalMatch[1];
471 globalKeyframes = true;
472 } else if (localMatch) {
473 atRule.params = localMatch[0];
474 globalKeyframes = false;
475 } else if (!globalMode) {
476 if (atRule.params && !localAliasMap.has(atRule.params)) {
477 atRule.params = ":local(" + atRule.params + ")";
478 }
479 }
480
481 atRule.walkDecls((declaration) => {
482 localizeDeclaration(declaration, {
483 localAliasMap,
484 options: options,
485 global: globalKeyframes,
486 });
487 });
488 } else if (atRule.nodes) {
489 atRule.nodes.forEach((declaration) => {
490 if (declaration.type === "decl") {
491 localizeDeclaration(declaration, {
492 localAliasMap,
493 options: options,
494 global: globalMode,
495 });
496 }
497 });
498 }
499 });
500
501 root.walkRules((rule) => {
502 if (
503 rule.parent &&
504 rule.parent.type === "atrule" &&
505 /keyframes$/i.test(rule.parent.name)
506 ) {
507 // ignore keyframe rules
508 return;
509 }
510
511 const context = localizeNode(rule, options.mode, localAliasMap);
512
513 context.options = options;
514 context.localAliasMap = localAliasMap;
515
516 if (pureMode && context.hasPureGlobals) {
517 throw rule.error(
518 'Selector "' +
519 rule.selector +
520 '" is not pure ' +
521 "(pure selectors must contain at least one local class or id)"
522 );
523 }
524
525 rule.selector = context.selector;
526
527 // Less-syntax mixins parse as rules with no nodes
528 if (rule.nodes) {
529 rule.nodes.forEach((declaration) =>
530 localizeDeclaration(declaration, context)
531 );
532 }
533 });
534 },
535 };
536 },
537 };
538};
539module.exports.postcss = true;
Note: See TracBrowser for help on using the repository browser.