Skip to content

Commit f7e2bb9

Browse files
committed
fix: resolve nested variables
1 parent 8e9439d commit f7e2bb9

File tree

4 files changed

+223
-166
lines changed

4 files changed

+223
-166
lines changed

packages/css-variables-language-server/src/CSSVariableManager.ts

Lines changed: 119 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,62 @@
1-
import { Range, Color, Location, Position } from 'vscode-languageserver/node';
2-
import * as fs from 'fs';
3-
import fastGlob from 'fast-glob';
4-
import * as culori from 'culori';
5-
import axios from 'axios';
6-
import postcss from 'postcss';
7-
import { pathToFileURL } from 'url';
8-
import path from 'path';
9-
import postcssSCSS from 'postcss-scss';
10-
import postcssLESS from 'postcss-less';
11-
import CacheManager from './CacheManager';
12-
import isColor from './utils/isColor';
13-
import { culoriColorToVscodeColor } from './utils/culoriColorToVscodeColor';
1+
import { Range, Color, Location, Position } from "vscode-languageserver/node";
2+
import * as fs from "fs";
3+
import fastGlob from "fast-glob";
4+
import * as culori from "culori";
5+
import axios from "axios";
6+
import postcss from "postcss";
7+
import { pathToFileURL } from "url";
8+
import path from "path";
9+
import postcssSCSS from "postcss-scss";
10+
import postcssLESS from "postcss-less";
11+
import CacheManager from "./CacheManager";
12+
import isColor from "./utils/isColor";
13+
import { culoriColorToVscodeColor } from "./utils/culoriColorToVscodeColor";
1414

1515
export type CSSSymbol = {
16-
name: string
17-
value: string
18-
}
16+
name: string;
17+
value: string;
18+
};
1919

2020
export type CSSVariable = {
21-
symbol: CSSSymbol
22-
definition: Location
23-
color?: Color
24-
}
21+
symbol: CSSSymbol;
22+
definition: Location;
23+
color?: Color;
24+
};
2525

2626
export interface CSSVariablesSettings {
27-
lookupFiles: string[]
28-
blacklistFolders: string[]
27+
lookupFiles: string[];
28+
blacklistFolders: string[];
2929
}
3030

3131
// The global settings, used when the `workspace/configuration` request is not supported by the client.
3232
// Please note that this is not the case when using this server with the client provided in this example
3333
// but could happen with other clients.
3434
export const defaultSettings: CSSVariablesSettings = {
35-
lookupFiles: ['**/*.less', '**/*.scss', '**/*.sass', '**/*.css'],
35+
lookupFiles: ["**/*.less", "**/*.scss", "**/*.sass", "**/*.css"],
3636
blacklistFolders: [
37-
'**/.cache',
38-
'**/.DS_Store',
39-
'**/.git',
40-
'**/.hg',
41-
'**/.next',
42-
'**/.svn',
43-
'**/bower_components',
44-
'**/CVS',
45-
'**/dist',
46-
'**/node_modules',
47-
'**/tests',
48-
'**/tmp',
37+
"**/.cache",
38+
"**/.DS_Store",
39+
"**/.git",
40+
"**/.hg",
41+
"**/.next",
42+
"**/.svn",
43+
"**/bower_components",
44+
"**/CVS",
45+
"**/dist",
46+
"**/node_modules",
47+
"**/tests",
48+
"**/tmp",
4949
],
5050
};
5151

5252
const getAST = (filePath: string, content: string) => {
5353
const fileExtension = path.extname(filePath);
5454

55-
if (fileExtension === '.less') {
55+
if (fileExtension === ".less") {
5656
return postcssLESS.parse(content);
5757
}
58-
59-
if (fileExtension === '.scss') {
58+
59+
if (fileExtension === ".scss") {
6060
return postcssSCSS.parse(content);
6161
}
6262

@@ -66,12 +66,72 @@ const getAST = (filePath: string, content: string) => {
6666
export default class CSSVariableManager {
6767
private cacheManager = new CacheManager<CSSVariable>();
6868

69+
private resolveCachedVariables = () => {
70+
for (const filePath of this.cacheManager.getFiles()) {
71+
this.cacheManager.getAll(filePath).forEach((variable, key) => {
72+
this.setCssVariable(
73+
key,
74+
this.resolveRecursiveVariables(variable.symbol.value),
75+
filePath,
76+
variable.definition.range
77+
);
78+
});
79+
}
80+
};
81+
82+
public resolveRecursiveVariables = (value: string) => {
83+
for (let i = 0; i < 20; i++) {
84+
const variableReference = value.match(
85+
/^var\(\s*([a-zA-Z0-9-]+)\s*\)$/
86+
)?.[1];
87+
if (!variableReference) {
88+
break;
89+
}
90+
const newValue = this.cacheManager.get(variableReference)?.symbol?.value;
91+
92+
if (newValue) {
93+
value = newValue;
94+
continue;
95+
}
96+
97+
break;
98+
}
99+
return value;
100+
};
101+
102+
public setCssVariable = (
103+
prop: string,
104+
value: string,
105+
filePath: string,
106+
range: Range
107+
) => {
108+
const variable: CSSVariable = {
109+
symbol: {
110+
name: prop,
111+
value: value,
112+
},
113+
definition: {
114+
uri: filePath,
115+
range: range,
116+
},
117+
};
118+
119+
const culoriColor = culori.parse(value);
120+
121+
if (culoriColor) {
122+
variable.color = culoriColorToVscodeColor(culoriColor);
123+
}
124+
125+
// add to cache
126+
this.cacheManager.set(filePath, prop, variable);
127+
};
128+
69129
public parseCSSVariablesFromText = async ({
70130
content,
71131
filePath,
72132
}: {
73-
content: string
74-
filePath: string
133+
content: string;
134+
filePath: string;
75135
}) => {
76136
try {
77137
// reset cache for this file
@@ -82,7 +142,7 @@ export default class CSSVariableManager {
82142

83143
const importUrls = [];
84144
ast.walkAtRules((atRule) => {
85-
if (atRule.name === 'import') {
145+
if (atRule.name === "import") {
86146
// only support absolute url for now
87147
const match = atRule.params.match(
88148
/['"](?<protocol>http|https):\/\/(?<url>.*?)['"]/
@@ -100,7 +160,7 @@ export default class CSSVariableManager {
100160
importUrls.map(async (url) => {
101161
try {
102162
const response = await axios(url, {
103-
responseType: 'text',
163+
responseType: "text",
104164
});
105165

106166
const cssText = await response.data;
@@ -116,37 +176,25 @@ export default class CSSVariableManager {
116176
);
117177

118178
ast.walkDecls((decl) => {
119-
if (decl.prop.startsWith('--')) {
120-
const variable: CSSVariable = {
121-
symbol: {
122-
name: decl.prop,
123-
value: decl.value,
124-
},
125-
definition: {
126-
uri: fileURI,
127-
range: Range.create(
128-
Position.create(
129-
decl.source.start.line - 1,
130-
decl.source.start.column - 1
131-
),
132-
Position.create(
133-
decl.source.end.line - 1,
134-
decl.source.end.column - 1
135-
)
179+
if (decl.prop.startsWith("--")) {
180+
this.setCssVariable(
181+
decl.prop,
182+
decl.value,
183+
fileURI,
184+
Range.create(
185+
Position.create(
186+
decl.source.start.line - 1,
187+
decl.source.start.column - 1
136188
),
137-
},
138-
};
139-
140-
const culoriColor = culori.parse(decl.value);
141-
142-
if (culoriColor) {
143-
variable.color = culoriColorToVscodeColor(culoriColor);
144-
}
145-
146-
// add to cache
147-
this.cacheManager.set(filePath, decl.prop, variable);
189+
Position.create(
190+
decl.source.end.line - 1,
191+
decl.source.end.column - 1
192+
)
193+
)
194+
);
148195
}
149196
});
197+
this.resolveCachedVariables();
150198
} catch (error) {
151199
console.error(filePath);
152200
}
@@ -165,7 +213,7 @@ export default class CSSVariableManager {
165213
}).then((files) => {
166214
return Promise.all(
167215
files.map((filePath) => {
168-
const content = fs.readFileSync(filePath, 'utf8');
216+
const content = fs.readFileSync(filePath, "utf8");
169217
return this.parseCSSVariablesFromText({
170218
content,
171219
filePath,
Lines changed: 45 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
22
* Cache Manager
3-
*
3+
*
44
* {
55
* src/styles/variables.css: {
66
* },
@@ -12,39 +12,47 @@
1212
*/
1313

1414
export default class CacheManager<T> {
15-
private cachedVariables: Map<string, Map<string, T>> = new Map();
16-
private allVariables: Map<string, T> = new Map();
17-
18-
public get(key: string, filePath?: string) {
19-
if (filePath) {
20-
return this.cachedVariables[filePath]?.get(key);
21-
}
22-
23-
return this.allVariables?.get(key);
24-
}
25-
26-
public getAll() {
27-
return this.allVariables;
28-
}
29-
30-
public set(filePath: string, key: string, value: T) {
31-
if (!this.cachedVariables[filePath]) {
32-
this.cachedVariables[filePath] = new Map();
33-
}
34-
35-
this.allVariables?.set(key, value);
36-
this.cachedVariables[filePath].set(key, value);
37-
}
38-
39-
public clearFileCache(filePath: string) {
40-
this.cachedVariables[filePath]?.forEach((_, key) => {
41-
this.allVariables?.delete(key);
42-
});
43-
this.cachedVariables[filePath]?.clear();
44-
}
45-
46-
public clearAllCache() {
47-
this.allVariables?.clear();
48-
this.cachedVariables.clear();
49-
}
50-
}
15+
private cachedVariables: Map<string, Map<string, T>> = new Map();
16+
private allVariables: Map<string, T> = new Map();
17+
18+
public get(key: string, filePath?: string) {
19+
if (filePath) {
20+
return this.cachedVariables.get(filePath)?.get(key);
21+
}
22+
23+
return this.allVariables?.get(key);
24+
}
25+
26+
public getAll(filePath?: string) {
27+
if (filePath) {
28+
return this.cachedVariables.get(filePath);
29+
}
30+
31+
return this.allVariables;
32+
}
33+
34+
public getFiles() {
35+
return this.cachedVariables.keys();
36+
}
37+
38+
public set(filePath: string, key: string, value: T) {
39+
if (!this.cachedVariables.get(filePath)) {
40+
this.cachedVariables.set(filePath, new Map());
41+
}
42+
43+
this.allVariables.set(key, value);
44+
this.cachedVariables.get(filePath).set(key, value);
45+
}
46+
47+
public clearFileCache(filePath: string) {
48+
this.cachedVariables.get(filePath)?.forEach((_, key) => {
49+
this.allVariables?.delete(key);
50+
});
51+
this.cachedVariables.get(filePath)?.clear();
52+
}
53+
54+
public clearAllCache() {
55+
this.allVariables?.clear();
56+
this.cachedVariables.clear();
57+
}
58+
}

0 commit comments

Comments
 (0)