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 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
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "css-ast-parser",
3
+ "version": "1.0.0",
4
+ "description": "Fast CSS parser with AST transformations",
5
+ "main": "src/index.js",
6
+ "author": "DragonDragging",
7
+ "license": "MIT"
8
+ }
@@ -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
@@ -0,0 +1,5 @@
1
+ const { parse } = require("./parser");
2
+ const { generate } = require("./generator");
3
+ const { walk } = require("./walker");
4
+
5
+ module.exports = { parse, generate, walk };
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
+ };