Skip to content

Commit d95bd38

Browse files
committed
Rewrite value parser
1 parent 6626424 commit d95bd38

File tree

4 files changed

+205
-146
lines changed

4 files changed

+205
-146
lines changed

package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@
6868
},
6969
"dependencies": {
7070
"icss-utils": "^3.0.1",
71-
"postcss": "^6.0.2"
71+
"lodash": "^4.17.4",
72+
"postcss": "^6.0.2",
73+
"postcss-value-parser": "^3.3.0"
7274
}
7375
}

src/index.js

+125-53
Original file line numberDiff line numberDiff line change
@@ -1,84 +1,156 @@
11
/* eslint-env node */
22
import postcss from "postcss";
3+
import valueParser from "postcss-value-parser";
34
import {
45
replaceSymbols,
56
replaceValueSymbols,
67
extractICSS,
78
createICSSRules
89
} from "icss-utils";
10+
import findLastIndex from "lodash/findLastIndex";
11+
import dropWhile from "lodash/dropWhile";
12+
import dropRightWhile from "lodash/dropRightWhile";
13+
import fromPairs from "lodash/fromPairs";
914

1015
const plugin = "postcss-modules-values";
1116

12-
const matchImports = /^(.+?|\([\s\S]+?\))\s+from\s+("[^"]*"|'[^']*'|[\w-]+)$/;
13-
const matchValueDefinition = /(?:\s+|^)([\w-]+):?\s+(.+?)\s*$/g;
14-
const matchImport = /^([\w-]+)(?:\s+as\s+([\w-]+))?/;
17+
const chunkBy = (collection, iteratee) =>
18+
collection.reduce(
19+
(acc, item) =>
20+
iteratee(item)
21+
? [...acc, []]
22+
: [...acc.slice(0, -1), [...acc[acc.length - 1], item]],
23+
[[]]
24+
);
25+
26+
const isWord = node => node.type === "word";
27+
28+
const isDiv = node => node.type === "div";
29+
30+
const isSpace = node => node.type === "space";
31+
32+
const isNotSpace = node => !isSpace(node);
33+
34+
const isFromWord = node => isWord(node) && node.value === "from";
35+
36+
const isAsWord = node => isWord(node) && node.value === "as";
37+
38+
const isComma = node => isDiv(node) && node.value === ",";
39+
40+
const isColon = node => isDiv(node) && node.value === ":";
41+
42+
const isInitializer = node => isColon(node) || isSpace(node);
43+
44+
const trimNodes = nodes => dropWhile(dropRightWhile(nodes, isSpace), isSpace);
45+
46+
const getPathValue = nodes =>
47+
nodes.length === 1 && nodes[0].type === "string" ? nodes[0].value : null;
48+
49+
const ensurePairsList = valuesNodes =>
50+
valuesNodes.length === 1 && valuesNodes[0].type === "function"
51+
? valuesNodes[0].nodes
52+
: valuesNodes;
53+
54+
const getAliasesPairs = valuesNodes =>
55+
chunkBy(ensurePairsList(valuesNodes), isComma).map(pairNodes => {
56+
const nodes = pairNodes.filter(isNotSpace);
57+
if (nodes.length === 1 && isWord(nodes[0])) {
58+
return [nodes[0].value, nodes[0].value];
59+
}
60+
if (
61+
nodes.length === 3 &&
62+
isWord(nodes[0]) &&
63+
isAsWord(nodes[1]) &&
64+
isWord(nodes[2])
65+
) {
66+
return [nodes[0].value, nodes[2].value];
67+
}
68+
return null;
69+
});
70+
71+
const parse = value => {
72+
const parsed = valueParser(value).nodes;
73+
const fromIndex = findLastIndex(parsed, isFromWord);
74+
if (fromIndex === -1) {
75+
if (parsed.length > 2 && isWord(parsed[0]) && isInitializer(parsed[1])) {
76+
return {
77+
type: "value",
78+
name: parsed[0].value,
79+
value: valueParser.stringify(trimNodes(parsed.slice(2)))
80+
};
81+
}
82+
return null;
83+
}
84+
const pairs = getAliasesPairs(trimNodes(parsed.slice(0, fromIndex)));
85+
const path = getPathValue(trimNodes(parsed.slice(fromIndex + 1)));
86+
if (pairs.every(Boolean) && path) {
87+
return {
88+
type: "import",
89+
pairs,
90+
path
91+
};
92+
}
93+
return null;
94+
};
1595

1696
const getAliasName = (name, index) =>
1797
`__value__${name.replace(/\W/g, "_")}__${index}`;
1898

99+
// TODO forbig '.' and '#' in names
100+
19101
module.exports = postcss.plugin(plugin, () => (css, result) => {
20102
const { icssImports, icssExports } = extractICSS(css);
21103
let importIndex = 0;
22104
const createImportedName = (path, name) => {
23105
const importedName = getAliasName(name, importIndex);
24-
if (icssImports[path] && icssImports[path][importedName]) {
25-
importIndex += 1;
26-
return createImportedName(path, name);
27-
}
28106
importIndex += 1;
29107
return importedName;
30108
};
31109

32-
const addDefinition = atRule => {
33-
let matches;
34-
while ((matches = matchValueDefinition.exec(atRule.params))) {
35-
let [, key, value] = matches;
36-
// Add to the definitions, knowing that values can refer to each other
37-
icssExports[key] = replaceValueSymbols(value, icssExports);
38-
atRule.remove();
39-
}
40-
};
41-
42-
const addImport = atRule => {
43-
let matches = matchImports.exec(atRule.params);
44-
if (matches) {
45-
const aliasesString = matches[1];
46-
let path = matches[2];
47-
path = path[0] === "'" || path[0] === '"' ? path.slice(1, -1) : path;
48-
let aliases = aliasesString
49-
.replace(/^\(\s*([\s\S]+)\s*\)$/, "$1")
50-
.split(/\s*,\s*/)
51-
.map(alias => {
52-
let tokens = matchImport.exec(alias);
53-
if (tokens) {
54-
let [, theirName, myName = theirName] = tokens;
55-
let importedName = createImportedName(path, myName);
56-
icssExports[myName] = importedName;
57-
return { theirName, importedName };
58-
} else {
59-
throw new Error(`@import statement "${alias}" is invalid!`);
60-
}
61-
})
62-
.reduce((acc, { theirName, importedName }) => {
63-
acc[importedName] = theirName;
64-
return acc;
65-
}, {});
66-
icssImports[path] = Object.assign({}, icssImports[path], aliases);
67-
atRule.remove();
68-
}
69-
};
70-
71-
/* Look at all the @value statements and treat them as locals or as imports */
72110
css.walkAtRules("value", atRule => {
73-
if (matchImports.exec(atRule.params)) {
74-
addImport(atRule);
111+
if (atRule.params.indexOf("@value") !== -1) {
112+
result.warn(`Invalid value definition "${atRule.params}"`, {
113+
node: atRule
114+
});
75115
} else {
76-
if (atRule.params.indexOf("@value") !== -1) {
77-
result.warn("Invalid value definition: " + atRule.params);
116+
const parsed = parse(atRule.params);
117+
if (parsed) {
118+
if (parsed.type === "value") {
119+
if (icssExports[parsed.name]) {
120+
result.warn(`"${parsed.name}" value already declared`, {
121+
node: atRule
122+
});
123+
}
124+
icssExports[parsed.name] = replaceValueSymbols(
125+
parsed.value,
126+
icssExports
127+
);
128+
}
129+
if (parsed.type === "import") {
130+
const pairs = parsed.pairs.map(([key, value]) => {
131+
let importedName = createImportedName(parsed.path, value);
132+
if (icssExports[value]) {
133+
result.warn(`"${value}" value already declared`, {
134+
node: atRule
135+
});
136+
}
137+
icssExports[value] = importedName;
138+
return [importedName, key];
139+
});
140+
const aliases = fromPairs(pairs);
141+
icssImports[parsed.path] = Object.assign(
142+
{},
143+
icssImports[parsed.path],
144+
aliases
145+
);
146+
}
147+
} else {
148+
result.warn(`Invalid value definition "${atRule.params}"`, {
149+
node: atRule
150+
});
78151
}
79-
80-
addDefinition(atRule);
81152
}
153+
atRule.remove();
82154
});
83155

84156
replaceSymbols(css, icssExports);

0 commit comments

Comments
 (0)