css-ast-parser 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +118 -0
- package/package.json +8 -0
- package/src/generator.js +79 -0
- package/src/index.js +5 -0
- package/src/parser.js +178 -0
- package/src/walker.js +96 -0
package/README.md
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# CSS AST Parser
|
|
2
|
+
A fast and lightweight CSS parser with AST transformations and plugin-based architecture.
|
|
3
|
+
The goal of this project is to provide high performance CSS processing with minimal overhead, suitable for large stylesheets and real time transformations.
|
|
4
|
+
Built using: custom parser, generator and AST walker (tokenization stage was intentionally removed for improving performance)
|
|
5
|
+
|
|
6
|
+
## Features
|
|
7
|
+
Lightweight and fast parsing
|
|
8
|
+
AST-based transformations
|
|
9
|
+
Plugin system (similar to PostCSS/Babel)
|
|
10
|
+
Safe node mutation (remove/replace during traversal)
|
|
11
|
+
Single-line minified output
|
|
12
|
+
Zero dependencies
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
Clone the repository
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
```js
|
|
20
|
+
const fs = require("fs");
|
|
21
|
+
|
|
22
|
+
const { parse } = require("./src/parser");
|
|
23
|
+
const { generate } = require("./src/generator");
|
|
24
|
+
const { walk } = require("./src/walker");
|
|
25
|
+
|
|
26
|
+
const plugins = [
|
|
27
|
+
require("./plugins/removeBackground")
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const css = fs.readFileSync("./input.css", "utf-8");
|
|
31
|
+
|
|
32
|
+
const ast = parse(css);
|
|
33
|
+
walk(ast, plugins);
|
|
34
|
+
|
|
35
|
+
const out = generate(ast);
|
|
36
|
+
fs.writeFileSync("./output.css", out);
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
## How It Works
|
|
42
|
+
The processing pipeline:
|
|
43
|
+
CSS -> parse() -> AST -> walk() -> generate() -> CSS
|
|
44
|
+
parse -> converts CSS into AST
|
|
45
|
+
walk -> applies plugins and mutates AST
|
|
46
|
+
generate -> converts AST back to CSS
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
## AST Structure
|
|
51
|
+
Example node:
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"type": "decl",
|
|
55
|
+
"prop": "color",
|
|
56
|
+
"value": "red"
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Node types:
|
|
61
|
+
rule - selector block
|
|
62
|
+
decl - declaration
|
|
63
|
+
atrule - @rules
|
|
64
|
+
comment - comments
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
## Plugins
|
|
69
|
+
Plugins return visitors.
|
|
70
|
+
|
|
71
|
+
### Example: remove background-color
|
|
72
|
+
```js
|
|
73
|
+
module.exports = function () {
|
|
74
|
+
return {
|
|
75
|
+
decl: {
|
|
76
|
+
enter(path) {
|
|
77
|
+
if (path.isDecl("background-color")) {
|
|
78
|
+
path.remove();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Path API
|
|
87
|
+
path.remove() — remove node
|
|
88
|
+
path.replace(node) — replace node
|
|
89
|
+
path.setProp(v) — change property
|
|
90
|
+
path.setValue(v) — change value
|
|
91
|
+
|
|
92
|
+
Helpers:
|
|
93
|
+
path.isDecl(name)
|
|
94
|
+
path.isRule(selector)
|
|
95
|
+
path.isAtRule(name)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
## Transformation Pipeline
|
|
100
|
+
1. Read CSS
|
|
101
|
+
2. Parse AST
|
|
102
|
+
3. Apply plugins
|
|
103
|
+
4. Generate optimized CSS
|
|
104
|
+
|
|
105
|
+
## Performance
|
|
106
|
+
This project is optimized for speed and low memory usage.
|
|
107
|
+
Key points:
|
|
108
|
+
fast loops (no forEach/map)
|
|
109
|
+
array join instead of string concat
|
|
110
|
+
minimal allocations
|
|
111
|
+
safe mutation traversal
|
|
112
|
+
Designed to handle large CSS files efficiently.
|
|
113
|
+
|
|
114
|
+
## Author
|
|
115
|
+
DragonDragging
|
|
116
|
+
|
|
117
|
+
## Contact
|
|
118
|
+
If you find any bugs, please contact me on Discord with a detailed explanation: @dragondragging
|
package/package.json
ADDED
package/src/generator.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
function generate(ast) {
|
|
2
|
+
const out = [];
|
|
3
|
+
|
|
4
|
+
function gen(nodes) {
|
|
5
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
6
|
+
const n = nodes[i];
|
|
7
|
+
|
|
8
|
+
// CSS rule: selector { decls }
|
|
9
|
+
if (n.type === "rule") {
|
|
10
|
+
if (!n.selector) continue;
|
|
11
|
+
|
|
12
|
+
out.push(n.selector, "{");
|
|
13
|
+
|
|
14
|
+
const children = n.nodes;
|
|
15
|
+
if (children) {
|
|
16
|
+
for (let j = 0; j < children.length; j++) {
|
|
17
|
+
const d = children[j];
|
|
18
|
+
|
|
19
|
+
// inline comment
|
|
20
|
+
if (d.type === "comment") {
|
|
21
|
+
out.push("/*", d.value, "*/");
|
|
22
|
+
}
|
|
23
|
+
// declaration
|
|
24
|
+
else if (d.type === "decl") {
|
|
25
|
+
out.push(d.prop, ":", d.value, ";");
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
out.push("}");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// @rule (media, property, etc.)
|
|
34
|
+
else if (n.type === "atrule") {
|
|
35
|
+
if (!n.name) continue;
|
|
36
|
+
|
|
37
|
+
out.push("@", n.name);
|
|
38
|
+
|
|
39
|
+
if (n.query) out.push(" ", n.query);
|
|
40
|
+
|
|
41
|
+
// no body (@import etc.)
|
|
42
|
+
if (!n.nodes) {
|
|
43
|
+
out.push(";");
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
out.push("{");
|
|
48
|
+
|
|
49
|
+
const children = n.nodes;
|
|
50
|
+
for (let j = 0; j < children.length; j++) {
|
|
51
|
+
const d = children[j];
|
|
52
|
+
|
|
53
|
+
if (d.type === "comment") {
|
|
54
|
+
out.push("/*", d.value, "*/");
|
|
55
|
+
}
|
|
56
|
+
else if (d.type === "decl") {
|
|
57
|
+
out.push(d.prop, ":", d.value, ";");
|
|
58
|
+
}
|
|
59
|
+
// nested rule / atrule
|
|
60
|
+
else {
|
|
61
|
+
gen([d]);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
out.push("}");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// comment
|
|
69
|
+
else if (n.type === "comment") {
|
|
70
|
+
out.push("/*", n.value, "*/");
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
gen(ast.nodes);
|
|
76
|
+
return out.join(""); // single allocation
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = { generate };
|
package/src/index.js
ADDED
package/src/parser.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
function parse(css) {
|
|
2
|
+
let i = 0;
|
|
3
|
+
const len = css.length;
|
|
4
|
+
|
|
5
|
+
// fast whitespace check
|
|
6
|
+
function isWS(c) {
|
|
7
|
+
return c === 32 || c === 10 || c === 9 || c === 13;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// skip whitespace
|
|
11
|
+
function skipWS() {
|
|
12
|
+
while (i < len && isWS(css.charCodeAt(i))) i++;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// read /* comment */
|
|
16
|
+
function readComment() {
|
|
17
|
+
i += 2;
|
|
18
|
+
let start = i;
|
|
19
|
+
|
|
20
|
+
while (i < len && !(css[i] === "*" && css[i + 1] === "/")) i++;
|
|
21
|
+
|
|
22
|
+
const value = css.slice(start, i).trim();
|
|
23
|
+
i += 2;
|
|
24
|
+
|
|
25
|
+
return { type: "comment", value };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// collapse whitespace
|
|
29
|
+
function clean(str) {
|
|
30
|
+
return str.replace(/\s+/g, " ").trim();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// read selector until {
|
|
34
|
+
function readSelector() {
|
|
35
|
+
let start = i;
|
|
36
|
+
while (i < len && css[i] !== "{") i++;
|
|
37
|
+
return clean(css.slice(start, i));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// read property name
|
|
41
|
+
function readProp() {
|
|
42
|
+
let start = i;
|
|
43
|
+
while (i < len && css[i] !== ":" && css[i] !== "}") i++;
|
|
44
|
+
return clean(css.slice(start, i));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// read value (supports functions like rgb())
|
|
48
|
+
function readValue() {
|
|
49
|
+
let start = i;
|
|
50
|
+
let depth = 0;
|
|
51
|
+
|
|
52
|
+
while (i < len) {
|
|
53
|
+
const c = css[i];
|
|
54
|
+
|
|
55
|
+
if (c === "(") depth++;
|
|
56
|
+
else if (c === ")") depth--;
|
|
57
|
+
|
|
58
|
+
if (depth === 0 && (c === ";" || c === "}")) break;
|
|
59
|
+
|
|
60
|
+
i++;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return clean(css.slice(start, i));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// parse declarations inside {}
|
|
67
|
+
function parseDecls() {
|
|
68
|
+
const nodes = [];
|
|
69
|
+
|
|
70
|
+
while (i < len) {
|
|
71
|
+
skipWS();
|
|
72
|
+
if (i >= len) break;
|
|
73
|
+
|
|
74
|
+
// end block
|
|
75
|
+
if (css[i] === "}") {
|
|
76
|
+
i++;
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// comment
|
|
81
|
+
if (css[i] === "/" && css[i + 1] === "*") {
|
|
82
|
+
nodes.push(readComment());
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const prop = readProp();
|
|
87
|
+
|
|
88
|
+
if (css[i] === "}") {
|
|
89
|
+
i++;
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
i++; // skip :
|
|
94
|
+
|
|
95
|
+
const value = readValue();
|
|
96
|
+
if (css[i] === ";") i++;
|
|
97
|
+
|
|
98
|
+
nodes.push({ type: "decl", prop, value });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return nodes;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// parse rules / atrules
|
|
105
|
+
function parseRules() {
|
|
106
|
+
const nodes = [];
|
|
107
|
+
|
|
108
|
+
// atrules with declarations instead of nested rules
|
|
109
|
+
const declAtrules = new Set(["property", "font-face", "page"]);
|
|
110
|
+
|
|
111
|
+
while (i < len) {
|
|
112
|
+
skipWS();
|
|
113
|
+
if (i >= len) break;
|
|
114
|
+
|
|
115
|
+
// end block
|
|
116
|
+
if (css[i] === "}") {
|
|
117
|
+
i++;
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// comment
|
|
122
|
+
if (css[i] === "/" && css[i + 1] === "*") {
|
|
123
|
+
nodes.push(readComment());
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// @rule
|
|
128
|
+
if (css[i] === "@") {
|
|
129
|
+
i++;
|
|
130
|
+
|
|
131
|
+
let start = i;
|
|
132
|
+
while (i < len && !isWS(css.charCodeAt(i)) && css[i] !== "{" && css[i] !== ";") i++;
|
|
133
|
+
const name = css.slice(start, i);
|
|
134
|
+
|
|
135
|
+
skipWS();
|
|
136
|
+
|
|
137
|
+
let queryStart = i;
|
|
138
|
+
while (i < len && css[i] !== "{" && css[i] !== ";") i++;
|
|
139
|
+
const query = clean(css.slice(queryStart, i));
|
|
140
|
+
|
|
141
|
+
// no block
|
|
142
|
+
if (css[i] === ";") {
|
|
143
|
+
i++;
|
|
144
|
+
nodes.push({ type: "atrule", name, query, nodes: null });
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
i++; // {
|
|
149
|
+
|
|
150
|
+
const children = declAtrules.has(name)
|
|
151
|
+
? parseDecls()
|
|
152
|
+
: parseRules();
|
|
153
|
+
|
|
154
|
+
nodes.push({ type: "atrule", name, query, nodes: children });
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// normal rule
|
|
159
|
+
const selector = readSelector();
|
|
160
|
+
i++; // {
|
|
161
|
+
|
|
162
|
+
nodes.push({
|
|
163
|
+
type: "rule",
|
|
164
|
+
selector,
|
|
165
|
+
nodes: parseDecls()
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return nodes;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
type: "stylesheet",
|
|
174
|
+
nodes: parseRules()
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
module.exports = { parse };
|
package/src/walker.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
function walk(ast, plugins = []) {
|
|
2
|
+
const visitors = plugins.map(p => p());
|
|
3
|
+
function createPath(node, parent, container, index) {
|
|
4
|
+
let removed = false;
|
|
5
|
+
|
|
6
|
+
return {
|
|
7
|
+
node,
|
|
8
|
+
parent,
|
|
9
|
+
// remove current node safely
|
|
10
|
+
remove() {
|
|
11
|
+
if (!container) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
container.splice(index, 1);
|
|
15
|
+
removed = true;
|
|
16
|
+
},
|
|
17
|
+
// replace current node
|
|
18
|
+
replace(newNode) {
|
|
19
|
+
if (!container) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
container[index] = newNode;
|
|
23
|
+
this.node = newNode;
|
|
24
|
+
},
|
|
25
|
+
// mutate decl
|
|
26
|
+
setProp(v) {
|
|
27
|
+
if (node.type === "decl") {
|
|
28
|
+
node.prop = v;
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
setValue(v) {
|
|
32
|
+
if (node.type === "decl") {
|
|
33
|
+
node.value = v;
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
// helpers
|
|
37
|
+
isDecl(name) {
|
|
38
|
+
return node.type === "decl" && (!name || node.prop === name);
|
|
39
|
+
},
|
|
40
|
+
isRule(sel) {
|
|
41
|
+
return node.type === "rule" && (!sel || node.selector === sel);
|
|
42
|
+
},
|
|
43
|
+
isAtRule(name) {
|
|
44
|
+
return node.type === "atrule" && (!name || node.name === name);
|
|
45
|
+
},
|
|
46
|
+
// internal flag
|
|
47
|
+
removed: () => removed
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function visit(node, parent, container, index) {
|
|
52
|
+
const path = createPath(node, parent, container, index);
|
|
53
|
+
|
|
54
|
+
// enter phase
|
|
55
|
+
for (let i = 0; i < visitors.length; i++) {
|
|
56
|
+
const v = visitors[i];
|
|
57
|
+
const fn = v[node.type];
|
|
58
|
+
if (fn && fn.enter) {
|
|
59
|
+
fn.enter(path);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// if removed in enter → stop
|
|
64
|
+
if (path.removed()) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// traverse children safely (handles mutations)
|
|
69
|
+
if (node.nodes && node.nodes.length) {
|
|
70
|
+
const list = node.nodes;
|
|
71
|
+
for (let i = 0; i < list.length;) {
|
|
72
|
+
const child = list[i];
|
|
73
|
+
visit(child, node, list, i);
|
|
74
|
+
|
|
75
|
+
// if current child removed don't increment
|
|
76
|
+
if (list[i] === child) {
|
|
77
|
+
i++;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// exit phase
|
|
83
|
+
for (let i = 0; i < visitors.length; i++) {
|
|
84
|
+
const v = visitors[i];
|
|
85
|
+
const fn = v[node.type];
|
|
86
|
+
if (fn && fn.exit) {
|
|
87
|
+
fn.exit(path);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
visit(ast, null, null, null);
|
|
93
|
+
}
|
|
94
|
+
module.exports = {
|
|
95
|
+
walk
|
|
96
|
+
};
|