Skip to content

Commit 963e95f

Browse files
committed
Refactor selector localizer and disallow broad modifier without spaces around
1 parent f9ffa40 commit 963e95f

File tree

2 files changed

+97
-106
lines changed

2 files changed

+97
-106
lines changed

src/index.js

Lines changed: 83 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -4,125 +4,104 @@ import Tokenizer from "css-selector-tokenizer";
44

55
const plugin = "postcss-modules-local-by-default";
66

7-
function normalizeNodeArray(nodes) {
8-
var array = [];
9-
nodes.forEach(x => {
10-
if (Array.isArray(x)) {
11-
normalizeNodeArray(x).forEach(item => {
12-
array.push(item);
13-
});
14-
} else if (x) {
15-
array.push(x);
16-
}
17-
});
18-
if (array.length > 0 && array[array.length - 1].type === "spacing") {
19-
array.pop();
20-
}
21-
return array;
22-
}
7+
const trimNodes = nodes => {
8+
const firstIndex = nodes.findIndex(node => node.type !== "spacing");
9+
const lastIndex = nodes
10+
.slice()
11+
.reverse()
12+
.findIndex(node => node.type !== "spacing");
13+
return nodes.slice(firstIndex, nodes.length - lastIndex);
14+
};
2315

24-
function localizeNode(node, context) {
25-
if (context.ignoreNextSpacing && node.type !== "spacing") {
26-
throw Error(`Missing whitespace after :${context.ignoreNextSpacing}`);
27-
}
28-
if (context.enforceNoSpacing && node.type === "spacing") {
29-
throw Error(`Missing whitespace before :${context.enforceNoSpacing}`);
30-
}
16+
const isSpacing = node => node.type === "spacing" || node.type === "operator";
3117

32-
switch (node.type) {
33-
case "selector":
34-
node.nodes = normalizeNodeArray(
35-
node.nodes.map(n => localizeNode(n, context))
36-
);
37-
break;
18+
const isModifier = node =>
19+
node.type === "pseudo-class" &&
20+
(node.name === "local" || node.name === "global");
3821

39-
case "spacing":
40-
if (context.ignoreNextSpacing) {
41-
context.ignoreNextSpacing = false;
42-
context.lastWasSpacing = false;
43-
context.enforceNoSpacing = false;
44-
return null;
45-
}
46-
context.lastWasSpacing = true;
47-
return node;
22+
function localizeNode(node, { mode, inside }) {
23+
const newNodes = node.nodes.reduce((acc, n, index, nodes) => {
24+
switch (n.type) {
25+
case "spacing":
26+
if (isModifier(nodes[index + 1])) {
27+
return [...acc, Object.assign({}, n, { value: "" })];
28+
}
29+
return [...acc, n];
4830

49-
case "operator":
50-
context.lastWasSpacing = true;
51-
return node;
31+
case "operator":
32+
if (isModifier(nodes[index + 1])) {
33+
return [...acc, Object.assign({}, n, { after: "" })];
34+
}
35+
return [...acc, n];
5236

53-
case "pseudo-class":
54-
if (node.name === "local" || node.name === "global") {
55-
if (context.inside) {
56-
throw Error(
57-
`A :${node.name} is not allowed inside of a :${context.inside}(...)`
58-
);
37+
case "pseudo-class":
38+
if (isModifier(n)) {
39+
if (inside) {
40+
throw Error(
41+
`A :${n.name} is not allowed inside of a :${inside}(...)`
42+
);
43+
}
44+
if (index !== 0 && !isSpacing(nodes[index - 1])) {
45+
throw Error(`Missing whitespace before :${n.name}`);
46+
}
47+
if (index !== nodes.length - 1 && !isSpacing(nodes[index + 1])) {
48+
throw Error(`Missing whitespace after :${n.name}`);
49+
}
50+
// set mode
51+
mode = n.name;
52+
return acc;
5953
}
60-
context.ignoreNextSpacing = context.lastWasSpacing ? node.name : false;
61-
context.enforceNoSpacing = context.lastWasSpacing ? false : node.name;
62-
context.global = node.name === "global";
63-
context.explicit = true;
64-
return null;
65-
}
66-
break;
54+
return [...acc, n];
6755

68-
case "nested-pseudo-class":
69-
var subContext;
70-
if (node.name === "local" || node.name === "global") {
71-
if (context.inside) {
72-
throw Error(
73-
`A :${node.name}(...) is not allowed inside of a :${context.inside}(...)`
74-
);
56+
case "nested-pseudo-class":
57+
if (n.name === "local" || n.name === "global") {
58+
if (inside) {
59+
throw Error(
60+
`A :${n.name}(...) is not allowed inside of a :${inside}(...)`
61+
);
62+
}
63+
return [
64+
...acc,
65+
...localizeNode(n.nodes[0], { mode: n.name, inside: n.name }).nodes
66+
];
67+
} else {
68+
return [
69+
...acc,
70+
Object.assign({}, n, {
71+
nodes: localizeNode(n.nodes[0], { mode, inside }).nodes
72+
})
73+
];
7574
}
76-
subContext = {
77-
global: node.name === "global",
78-
inside: node.name,
79-
hasLocals: false,
80-
explicit: true
81-
};
82-
node = node.nodes.map(n => localizeNode(n, subContext));
83-
// don't leak spacing
84-
node[0].before = undefined;
85-
node[node.length - 1].after = undefined;
86-
} else {
87-
subContext = {
88-
global: context.global,
89-
inside: context.inside,
90-
lastWasSpacing: true,
91-
hasLocals: false,
92-
explicit: context.explicit
93-
};
94-
node.nodes = node.nodes.map(n => localizeNode(n, subContext));
95-
}
96-
context.hasLocals = subContext.hasLocals;
97-
break;
9875

99-
case "id":
100-
case "class":
101-
if (!context.global) {
102-
node = {
103-
type: "nested-pseudo-class",
104-
name: "local",
105-
nodes: [node]
106-
};
107-
context.hasLocals = true;
108-
}
109-
break;
110-
}
76+
case "id":
77+
case "class":
78+
if (mode === "local") {
79+
return [
80+
...acc,
81+
{
82+
type: "nested-pseudo-class",
83+
name: "local",
84+
nodes: [n]
85+
}
86+
];
87+
}
88+
return [...acc, n];
89+
90+
default:
91+
return [...acc, n];
92+
}
93+
}, []);
11194

112-
// reset context
113-
context.lastWasSpacing = false;
114-
context.ignoreNextSpacing = false;
115-
context.enforceNoSpacing = false;
116-
return node;
95+
return Object.assign({}, node, { nodes: trimNodes(newNodes) });
11796
}
11897

11998
const localizeSelectors = (selectors, mode) => {
12099
const node = Tokenizer.parse(selectors);
121-
const global = mode === "global";
122-
node.nodes = node.nodes.map(n =>
123-
localizeNode(n, { global, lastWasSpacing: true, hasLocals: false })
100+
return Tokenizer.stringify(
101+
Object.assign({}, node, {
102+
nodes: node.nodes.map(n => localizeNode(n, { mode }))
103+
})
124104
);
125-
return Tokenizer.stringify(node);
126105
};
127106

128107
const walkRules = (css, callback) => {

test/test.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,6 @@ test("use correct spacing", () => {
237237
return runCSS({
238238
fixture: `
239239
.a :local .b {}
240-
.a:local.b {}
241240
.a:local(.b) {}
242241
.a:local( .b ) {}
243242
.a :local(.b) {}
@@ -252,7 +251,6 @@ test("use correct spacing", () => {
252251
.a :local(.b) {}
253252
.a:local(.b) {}
254253
.a:local(.b) {}
255-
.a:local(.b) {}
256254
.a :local(.b) {}
257255
.a :local(.b) {}
258256
:local(.a).b {}
@@ -341,6 +339,20 @@ test("throw on incorrect spacing with broad :local", () => {
341339
});
342340
});
343341

342+
test("throw on incorrect spacing with broad :local on both side", () => {
343+
return runError({
344+
fixture: ".foo:local.bar {}",
345+
error: /Missing whitespace before :local/
346+
});
347+
});
348+
349+
test("throw on incorrect spacing with broad :global on both side", () => {
350+
return runError({
351+
fixture: ".foo:global.bar {}",
352+
error: /Missing whitespace before :global/
353+
});
354+
});
355+
344356
test("pass through global element", () => {
345357
return runCSS({
346358
fixture: "input {}",

0 commit comments

Comments
 (0)