-
Notifications
You must be signed in to change notification settings - Fork 0
/
x-style-unnest.js
154 lines (150 loc) · 4.54 KB
/
x-style-unnest.js
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
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
(() => {
/**
* x-style plugin that unnests the css if the browser doesn't support nested
* selectors.
*/
var nestedSupported = CSS.supports("selector(&)");
var xstyle = window.xstyle;
/**
* Combine two selectors into a single selector for unnesting.
* @param {string} first - First selector
* @param {string} second - Second selector
* @returns {string} - Combined selector
* @example
* combineSelectors("div", "span") // "div span"
* combineSelectors("div", "& span") // "div span"
* combineSelectors("div", "&.foo") // "div.foo"
* combineSelectors("div", ".test &") // ".test div"
* combineSelectors("div", ".test &:hover &") // ".test div:hover div"
*/
var combineSelectors = (first, second) => {
var wsStart = second.match(/^\s*/)[0];
var wsEnd = second.match(/\s*$/)[0];
var i = 0;
var c;
first = first.trim();
second = second.trim();
// Check if first selector is a list of selectors separated by commas.
// If so, wrap the list in :is() pseudo-class.
// This check takes into account escaped characters, quoted strings and
// parentheses for other pseudo-classes such as :is() and :not().
while (i < first.length) {
c = first[i];
if (c === "\\") {
// Escape next character, skip it
i += 2;
} else if (c === '"' || c === "'") {
var endC = c;
// Skip quoted string
i++;
while (c !== endC) {
c = first[i];
i += c === "\\" ? 2 : 1;
}
i++;
} else if (c === "(") {
var braceCount = 1;
while (c !== ")" && braceCount > 0) {
c = first[i];
if (c === "(") braceCount++;
if (c === ")") braceCount--;
i++;
}
} else if (c === ",") {
// First selector is a list of selectors separated by commas.
// Wrap the list in :is() pseudo-class.
first = ":is(" + first + ")";
break;
} else {
i++;
}
}
// Split second selector into a list of selectors separated by commas.
const both = [...second.match(/(\\.|[^,])+/g)]
.map((selector) => {
selector = selector.trim();
// Replace & with first selector
var selectorUpdated = selector.replace(/(^|[^\\])(&)/g, "$1" + first);
if (selectorUpdated === selector) {
// If & was not found, append first selector to second selector
selectorUpdated = first + " " + selector;
}
return selectorUpdated;
})
.join(", ");
return wsStart + both + wsEnd;
};
/**
* Unnest nested CSS
* @param {string} css - CSS to unnest
* @returns {string} - Unnested CSS
*/
var unnestCSS = (css) => {
if (nestedSupported) return css;
// Remove comments
css = css.replace(/\/\*[^*]*\*+([^/][^*]*\*+)*\//g, "");
var context = "";
var out = "";
var i = 0;
var c;
var block = "";
var l = css.length;
var selectors = [];
var atRuleStack = [];
var emitBlock = () => {
if (!block.match(/\S/)) return;
out += selectors[selectors.length - 1] + "{" + block + "}";
block = "";
};
while (i < l) {
c = css[i];
if (c === "\\") {
// Escape next character
context += css.slice(i, i + 2);
i += 2;
} else if (c === ";") {
// Context is a property, add it to the block
block += context + c;
context = "";
i++;
} else if (c === "}") {
// Context is the last property, add it to the block and emit the block
block += context;
context = "";
emitBlock();
if (atRuleStack.pop()) {
out += c;
} else {
selectors.pop();
}
i++;
} else if (c === "{") {
// Context is a selector
// Emit the last block if there is one
// Combine the current selector with the parent selector
// Add the new selector to the list of selectors
emitBlock();
var currentSelector = selectors[selectors.length - 1];
var newSelector = context;
if (newSelector.match(/^\s*@/)) {
atRuleStack.push(true);
out += newSelector + c;
} else {
if (currentSelector) {
newSelector = combineSelectors(currentSelector, newSelector);
}
selectors.push(newSelector);
atRuleStack.push(false);
}
context = "";
i++;
} else {
context += c;
i++;
}
}
return out;
}
xstyle.post.push(unnestCSS);
xstyle.unnest = unnestCSS;
})();