Skip to content

Commit 91c8577

Browse files
committed
rewrite the whole thing
1 parent b8d5745 commit 91c8577

File tree

5 files changed

+243
-171
lines changed

5 files changed

+243
-171
lines changed

src/TreeNode.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/** @template T*/
2+
export class TreeNode {
3+
/** @param {string} name */
4+
constructor(name) {
5+
/** @type {string} */
6+
this.name = name
7+
/** @type {Map<string, TreeNode<T>>} */
8+
this.children = new Map()
9+
/** @type {T[]} */
10+
this.locations = [] // Store metadata for each location added
11+
}
12+
13+
/**
14+
*
15+
* @param {string[]} path
16+
* @param {string} name
17+
* @param {T} location
18+
*/
19+
add_child(path, name, location) {
20+
let current = this
21+
22+
// Traverse path to find the correct location
23+
path.forEach((segment) => {
24+
// @ts-expect-error Apparently, TypeScript doesn't know that current is a TreeNode
25+
current = current.children.get(segment)
26+
})
27+
28+
// If the item already exists, add the location to its metadata
29+
if (current.children.has(name)) {
30+
// @ts-expect-error Apparently, TypeScript doesn't know that current is a TreeNode
31+
current.children.get(name).locations.push(location)
32+
} else {
33+
// Otherwise, create the item and add the location
34+
const new_node = new TreeNode(name)
35+
new_node.locations.push(location)
36+
current.children.set(name, new_node)
37+
}
38+
}
39+
40+
/**
41+
* @typedef PlainObject
42+
* @property {string} name
43+
* @property {T[]} locations
44+
* @property {PlainObject[]} children
45+
*/
46+
47+
/**
48+
* Convert the tree to a plain object for easy testing
49+
* @returns {PlainObject}
50+
*/
51+
to_plain_object() {
52+
return {
53+
name: this.name,
54+
locations: this.locations,
55+
children: Array
56+
.from(this.children.values())
57+
.map((child) => child.to_plain_object()),
58+
}
59+
}
60+
}

src/index.js

Lines changed: 97 additions & 148 deletions
Original file line numberDiff line numberDiff line change
@@ -1,204 +1,153 @@
11
import * as csstree from 'css-tree'
2+
import { TreeNode } from './TreeNode.js'
23

34
/**
4-
* @typedef {Object} LayerTree
5-
* @property {string} name
6-
* @property {LayerTree[]} children
5+
* @typedef Location
6+
* @property {number} line
7+
* @property {number} column
8+
* @property {number} start
9+
* @property {number} end
710
*/
811

9-
class List {
10-
/** @type {string} */
11-
name
12-
/** @type {List[]} */
13-
children
14-
15-
/**
16-
* @param {string | undefined} name
17-
*/
18-
constructor(name = undefined) {
19-
this.name = name || 'root'
20-
this.children = []
21-
}
22-
23-
/** @param {string} name */
24-
has(name) {
25-
for (let child of this.children) {
26-
if (child.name === name) {
27-
return true
28-
}
29-
}
30-
return false
31-
}
32-
33-
/**
34-
*
35-
* @param {string} name
36-
* @returns
37-
*/
38-
push(name) {
39-
if (this.has(name) && name !== '<anonymous>') {
40-
return this.children.find((child) => child.name === name)
41-
}
42-
43-
let new_item = new List(name)
44-
this.children.push(new_item)
45-
return new_item
46-
}
47-
48-
/**
49-
* @returns {LayerTree}
50-
*/
51-
serialize() {
52-
return {
53-
name: this.name,
54-
children: this.children.map((child) => child.serialize()),
55-
}
56-
}
57-
}
58-
5912
/**
60-
* Get the parent Atrule for `childNode`
61-
* @param {import('css-tree').CssNode} ast The AST to search in
62-
* @param {import('css-tree').Atrule} childNode The Atrule we want to get the potential parent Atrule for
13+
* @param {import('css-tree').CssNode} node
14+
* @returns {Location | undefined}
6315
*/
64-
function get_parent_rule(ast, childNode) {
65-
let parent
66-
csstree.walk(ast, {
67-
visit: 'Atrule',
68-
enter: function (/** @type {import('css-tree').Atrule} */node) {
69-
if (node === childNode && this.atrule) {
70-
parent = this.atrule
71-
return this.break
72-
}
73-
},
74-
})
75-
return parent
16+
function get_location(node) {
17+
let loc = node.loc
18+
if (!loc) return
19+
return {
20+
line: loc.start.line,
21+
column: loc.start.column,
22+
start: loc.start.offset,
23+
end: loc.end.offset,
24+
}
7625
}
7726

78-
/**
79-
* @param {import('css-tree').AtrulePrelude | import('css-tree').Raw | null} prelude
80-
* @returns string
81-
*/
82-
function get_layer_name(prelude) {
83-
return prelude === null ? '<anonymous>' : csstree.generate(prelude)
27+
/** @param {import('css-tree').Atrule} node */
28+
function is_layer(node) {
29+
return node.name.toLowerCase() === 'layer'
8430
}
8531

8632
/**
87-
*
8833
* @param {import('css-tree').CssNode} ast
89-
* @param {import('css-tree').Atrule} atrule
90-
* @returns {string[]}
9134
*/
92-
function resolve_parent_tree(ast, atrule) {
93-
let stack = []
94-
95-
// @ts-expect-error Let me just do a while loop plz
96-
while ((atrule = get_parent_rule(ast, atrule))) {
97-
if (atrule.name === 'layer') {
98-
stack.unshift(get_layer_name(atrule.prelude))
99-
}
35+
export function get_tree_from_ast(ast) {
36+
/** @type {string[]} */
37+
let current_stack = []
38+
let root = new TreeNode('root')
39+
let anonymous_counter = 0
40+
41+
/** @returns {string} */
42+
function get_anonymous_id() {
43+
anonymous_counter++
44+
return `__anonymous-${anonymous_counter}__`
10045
}
10146

102-
return stack
103-
}
104-
105-
/**
106-
* @param {import('css-tree').CssNode} ast
107-
* @returns {string[][]}
108-
*/
109-
export function get_ast_tree(ast) {
110-
/** @type {string[][]} */
111-
let list = []
47+
/**
48+
* @param {import('css-tree').AtrulePrelude} prelude
49+
* @returns {string[]}
50+
*/
51+
function get_layer_names(prelude) {
52+
return csstree
53+
// @todo: fewer loops plz
54+
.generate(prelude)
55+
.split('.')
56+
.map((s) => s.trim())
57+
}
11258

11359
csstree.walk(ast, {
11460
visit: 'Atrule',
115-
enter: function (/** @type {import('css-tree').Atrule} */ node) {
116-
if (node.name === 'layer') {
117-
let layer_name = get_layer_name(node.prelude)
61+
enter(node) {
62+
if (is_layer(node)) {
63+
let location = get_location(node)
64+
65+
if (node.prelude === null) {
66+
let layer_name = get_anonymous_id()
67+
root.add_child(current_stack, layer_name, location)
68+
current_stack.push(layer_name)
69+
return
70+
}
11871

119-
// @layer first, second;
120-
if (node.block === null) {
121-
for (let name of layer_name.split(',')) {
122-
list.push([...resolve_parent_tree(ast, node), name.trim()])
72+
if (node.prelude.type === 'AtrulePrelude') {
73+
if (node.block === null) {
74+
// @ts-expect-error CSSTree types are not updated yet in @types/css-tree
75+
let prelude = csstree.findAll(node.prelude, n => n.type === 'Layer').map(n => n.name)
76+
for (let name of prelude) {
77+
root.add_child(current_stack, name, location)
78+
}
79+
} else {
80+
for (let layer_name of get_layer_names(node.prelude)) {
81+
root.add_child(current_stack, layer_name, location)
82+
current_stack.push(layer_name)
83+
}
12384
}
124-
125-
return this.skip
12685
}
86+
} else if (node.name.toLowerCase() === 'import' && node.prelude !== null && node.prelude.type === 'AtrulePrelude') {
87+
let location = get_location(node)
88+
let prelude = node.prelude
12789

128-
// @layer first { /* content */ }
129-
list.push([...resolve_parent_tree(ast, node), layer_name])
130-
return this.skip
131-
} else if (node.name === 'import' && node.prelude !== null) {
13290
// @import url("foo.css") layer(test);
91+
// OR
92+
// @import url("foo.css") layer(test.nested);
13393
// @ts-expect-error CSSTree types are not updated to v3 yet
134-
let layer = csstree.find(node.prelude, (pr_node) => pr_node.type === 'Layer')
94+
let layer = csstree.find(prelude, n => n.type === 'Layer')
13595
if (layer) {
13696
// @ts-expect-error CSSTree types are not updated to v3 yet
137-
list.push([layer.name])
97+
for (let layer_name of get_layer_names(layer)) {
98+
root.add_child(current_stack, layer_name, location)
99+
current_stack.push(layer_name)
100+
}
138101
return this.skip
139102
}
140103

141104
// @import url("foo.css") layer();
142-
let layer_fn = csstree.find(
143-
node.prelude,
144-
(pr_node) =>
145-
pr_node.type === 'Function' && pr_node.name.toLowerCase() === 'layer'
146-
)
105+
let layer_fn = csstree.find(prelude, n => n.type === 'Function' && n.name.toLowerCase() === 'layer')
147106
if (layer_fn) {
148-
list.push(['<anonymous>'])
107+
root.add_child([], get_anonymous_id(), location)
149108
return this.skip
150109
}
151110

152111
// @import url("foo.css") layer;
153-
let layer_keyword = csstree.find(
154-
node.prelude,
155-
(pre_node) =>
156-
pre_node.type === 'Identifier' && pre_node.name.toLowerCase() === 'layer'
157-
)
112+
let layer_keyword = csstree.find(prelude, n => n.type === 'Identifier' && n.name.toLowerCase() === 'layer')
158113
if (layer_keyword) {
159-
list.push(['<anonymous>'])
114+
root.add_child([], get_anonymous_id(), location)
160115
return this.skip
161116
}
162117
}
163-
return this.skip
164-
}
118+
},
119+
leave(node) {
120+
if (is_layer(node)) {
121+
if (node.prelude !== null && node.prelude.type === 'AtrulePrelude') {
122+
let layer_names = get_layer_names(node.prelude)
123+
for (let i = 0; i < layer_names.length; i++) {
124+
current_stack.pop()
125+
}
126+
} else {
127+
// pop the anonymous layer
128+
current_stack.pop()
129+
}
130+
} else if (node.name.toLowerCase() === 'import') {
131+
// clear the stack, imports can not be nested
132+
current_stack.length = 0
133+
}
134+
},
165135
})
166136

167-
return list
137+
return root.to_plain_object().children
168138
}
169139

170140
/**
171141
* @param {string} css
172-
* @returns {LayerTree[]}
173142
*/
174143
export function get_tree(css) {
175144
let ast = csstree.parse(css, {
176145
positions: true,
177146
parseAtrulePrelude: true,
178-
parseRulePrelude: false,
179147
parseValue: false,
148+
parseRulePrelude: false,
180149
parseCustomProperty: false,
181150
})
182-
let list_of_layers = get_ast_tree(ast).map((layer) => layer.join('.'))
183-
184-
let known = new List()
185-
186-
for (let name of list_of_layers) {
187-
if (name.includes('.')) {
188-
let parts = name.split('.')
189-
// @ts-expect-error Let me just do a while loop plz
190-
let last_item = known.push(parts.shift())
191-
192-
while (parts.length > 0 && last_item) {
193-
// @ts-expect-error Let me just do a while loop plz
194-
last_item = last_item.push(parts.shift())
195-
}
196-
197-
continue
198-
}
199-
200-
known.push(name)
201-
}
202151

203-
return known.children.map((child) => child.serialize())
152+
return get_tree_from_ast(ast)
204153
}

0 commit comments

Comments
 (0)