forked from darkreader/darkreader
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathparse-css.ts
More file actions
115 lines (99 loc) · 3.8 KB
/
Copy pathparse-css.ts
File metadata and controls
115 lines (99 loc) · 3.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
import {getOpenCloseRange, splitExcluding} from '../text';
import type {TextRange} from '../text';
import {removeCSSComments} from './css-text';
export interface ParsedDeclaration {
property: string;
value: string;
important: boolean;
}
export interface ParsedStyleRule {
selectors: string[];
declarations: ParsedDeclaration[];
}
export interface ParsedAtRule {
type: string;
query: string;
rules: Array<ParsedAtRule | ParsedStyleRule>;
}
export type ParsedCSS = Array<ParsedAtRule | ParsedStyleRule>;
export function parseCSS(cssText: string): ParsedCSS {
cssText = removeCSSComments(cssText);
cssText = cssText.trim();
if (!cssText) {
return [];
}
const rules: ParsedCSS = [];
// Find {...} ranges excluding inside of "...", [...] etc.
const excludeRanges = getTokenExclusionRanges(cssText);
const bracketRanges = getAllOpenCloseRanges(cssText, '{', '}', excludeRanges);
let ruleStart = 0;
bracketRanges.forEach((brackets) => {
const key = cssText.substring(ruleStart, brackets.start).trim();
const content = cssText.substring(brackets.start + 1, brackets.end - 1);
if (key.startsWith('@')) {
const typeEndIndex = key.search(/[\s\(]/);
const rule: ParsedAtRule = {
type: typeEndIndex < 0 ? key : key.substring(0, typeEndIndex),
query: typeEndIndex < 0 ? '' : key.substring(typeEndIndex).trim(),
rules: parseCSS(content),
};
rules.push(rule);
} else {
const rule: ParsedStyleRule = {
selectors: parseSelectors(key),
declarations: parseDeclarations(content),
};
rules.push(rule);
}
ruleStart = brackets.end;
});
return rules;
}
function getAllOpenCloseRanges(
input: string,
openToken: string,
closeToken: string,
excludeRanges: TextRange[] = [],
) {
const ranges: TextRange[] = [];
let i = 0;
let range: TextRange | null;
while ((range = getOpenCloseRange(input, i, openToken, closeToken, excludeRanges))) {
ranges.push(range);
i = range.end;
}
return ranges;
}
function getTokenExclusionRanges(cssText: string) {
const singleQuoteGoesFirst = cssText.indexOf("'") < cssText.indexOf('"');
const firstQuote = singleQuoteGoesFirst ? "'" : '"';
const secondQuote = singleQuoteGoesFirst ? '"' : "'";
const excludeRanges: TextRange[] = getAllOpenCloseRanges(cssText, firstQuote, firstQuote);
excludeRanges.push(...getAllOpenCloseRanges(cssText, secondQuote, secondQuote, excludeRanges));
excludeRanges.push(...getAllOpenCloseRanges(cssText, '[', ']', excludeRanges));
excludeRanges.push(...getAllOpenCloseRanges(cssText, '(', ')', excludeRanges));
return excludeRanges;
}
function parseSelectors(selectorText: string) {
const excludeRanges = getTokenExclusionRanges(selectorText);
return splitExcluding(selectorText, ',', excludeRanges);
}
function parseDeclarations(cssDeclarationsText: string) {
const declarations: ParsedDeclaration[] = [];
const excludeRanges = getTokenExclusionRanges(cssDeclarationsText);
splitExcluding(cssDeclarationsText, ';', excludeRanges).forEach((part) => {
const colonIndex = part.indexOf(':');
if (colonIndex > 0) {
const importantIndex = part.indexOf('!important');
declarations.push({
property: part.substring(0, colonIndex).trim(),
value: part.substring(colonIndex + 1, importantIndex > 0 ? importantIndex : part.length).trim(),
important: importantIndex > 0,
});
}
});
return declarations;
}
export function isParsedStyleRule(rule: ParsedAtRule | ParsedStyleRule): rule is ParsedStyleRule {
return 'selectors' in rule;
}