Skip to content

Rewrite #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Nov 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions src/TreeNode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/** @template T*/
export class TreeNode {
/** @param {string} name */
constructor(name) {
/** @type {string} */
this.name = name
/** @type {Map<string, TreeNode<T>>} */
this.children = new Map()
/** @type {T[]} */
this.locations = [] // Store metadata for each location added
}

/**
*
* @param {string[]} path
* @param {string} name
* @param {T} location
*/
add_child(path, name, location) {
let current = this

// Traverse path to find the correct location
path.forEach((segment) => {
// @ts-expect-error Apparently, TypeScript doesn't know that current is a TreeNode
current = current.children.get(segment)
})

// If the item already exists, add the location to its metadata
if (current.children.has(name)) {
// @ts-expect-error Apparently, TypeScript doesn't know that current is a TreeNode
current.children.get(name).locations.push(location)
} else {
// Otherwise, create the item and add the location
const new_node = new TreeNode(name)
new_node.locations.push(location)
current.children.set(name, new_node)
}
}

/**
* @typedef PlainObject
* @property {string} name
* @property {T[]} locations
* @property {PlainObject[]} children
*/

/**
* Convert the tree to a plain object for easy testing
* @returns {PlainObject}
*/
to_plain_object() {
return {
name: this.name,
locations: this.locations,
children: Array
.from(this.children.values())
.map((child) => child.to_plain_object()),
}
}
}
245 changes: 97 additions & 148 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,204 +1,153 @@
import * as csstree from 'css-tree'
import { TreeNode } from './TreeNode.js'

/**
* @typedef {Object} LayerTree
* @property {string} name
* @property {LayerTree[]} children
* @typedef Location
* @property {number} line
* @property {number} column
* @property {number} start
* @property {number} end
*/

class List {
/** @type {string} */
name
/** @type {List[]} */
children

/**
* @param {string | undefined} name
*/
constructor(name = undefined) {
this.name = name || 'root'
this.children = []
}

/** @param {string} name */
has(name) {
for (let child of this.children) {
if (child.name === name) {
return true
}
}
return false
}

/**
*
* @param {string} name
* @returns
*/
push(name) {
if (this.has(name) && name !== '<anonymous>') {
return this.children.find((child) => child.name === name)
}

let new_item = new List(name)
this.children.push(new_item)
return new_item
}

/**
* @returns {LayerTree}
*/
serialize() {
return {
name: this.name,
children: this.children.map((child) => child.serialize()),
}
}
}

/**
* Get the parent Atrule for `childNode`
* @param {import('css-tree').CssNode} ast The AST to search in
* @param {import('css-tree').Atrule} childNode The Atrule we want to get the potential parent Atrule for
* @param {import('css-tree').CssNode} node
* @returns {Location | undefined}
*/
function get_parent_rule(ast, childNode) {
let parent
csstree.walk(ast, {
visit: 'Atrule',
enter: function (/** @type {import('css-tree').Atrule} */node) {
if (node === childNode && this.atrule) {
parent = this.atrule
return this.break
}
},
})
return parent
function get_location(node) {
let loc = node.loc
if (!loc) return
return {
line: loc.start.line,
column: loc.start.column,
start: loc.start.offset,
end: loc.end.offset,
}
}

/**
* @param {import('css-tree').AtrulePrelude | import('css-tree').Raw | null} prelude
* @returns string
*/
function get_layer_name(prelude) {
return prelude === null ? '<anonymous>' : csstree.generate(prelude)
/** @param {import('css-tree').Atrule} node */
function is_layer(node) {
return node.name.toLowerCase() === 'layer'
}

/**
*
* @param {import('css-tree').CssNode} ast
* @param {import('css-tree').Atrule} atrule
* @returns {string[]}
*/
function resolve_parent_tree(ast, atrule) {
let stack = []

// @ts-expect-error Let me just do a while loop plz
while ((atrule = get_parent_rule(ast, atrule))) {
if (atrule.name === 'layer') {
stack.unshift(get_layer_name(atrule.prelude))
}
export function get_tree_from_ast(ast) {
/** @type {string[]} */
let current_stack = []
let root = new TreeNode('root')
let anonymous_counter = 0

/** @returns {string} */
function get_anonymous_id() {
anonymous_counter++
return `__anonymous-${anonymous_counter}__`
}

return stack
}

/**
* @param {import('css-tree').CssNode} ast
* @returns {string[][]}
*/
export function get_ast_tree(ast) {
/** @type {string[][]} */
let list = []
/**
* @param {import('css-tree').AtrulePrelude} prelude
* @returns {string[]}
*/
function get_layer_names(prelude) {
return csstree
// @todo: fewer loops plz
.generate(prelude)
.split('.')
.map((s) => s.trim())
}

csstree.walk(ast, {
visit: 'Atrule',
enter: function (/** @type {import('css-tree').Atrule} */ node) {
if (node.name === 'layer') {
let layer_name = get_layer_name(node.prelude)
enter(node) {
if (is_layer(node)) {
let location = get_location(node)

if (node.prelude === null) {
let layer_name = get_anonymous_id()
root.add_child(current_stack, layer_name, location)
current_stack.push(layer_name)
return
}

// @layer first, second;
if (node.block === null) {
for (let name of layer_name.split(',')) {
list.push([...resolve_parent_tree(ast, node), name.trim()])
if (node.prelude.type === 'AtrulePrelude') {
if (node.block === null) {
// @ts-expect-error CSSTree types are not updated yet in @types/css-tree
let prelude = csstree.findAll(node.prelude, n => n.type === 'Layer').map(n => n.name)
for (let name of prelude) {
root.add_child(current_stack, name, location)
}
} else {
for (let layer_name of get_layer_names(node.prelude)) {
root.add_child(current_stack, layer_name, location)
current_stack.push(layer_name)
}
}

return this.skip
}
} else if (node.name.toLowerCase() === 'import' && node.prelude !== null && node.prelude.type === 'AtrulePrelude') {
let location = get_location(node)
let prelude = node.prelude

// @layer first { /* content */ }
list.push([...resolve_parent_tree(ast, node), layer_name])
return this.skip
} else if (node.name === 'import' && node.prelude !== null) {
// @import url("foo.css") layer(test);
// OR
// @import url("foo.css") layer(test.nested);
// @ts-expect-error CSSTree types are not updated to v3 yet
let layer = csstree.find(node.prelude, (pr_node) => pr_node.type === 'Layer')
let layer = csstree.find(prelude, n => n.type === 'Layer')
if (layer) {
// @ts-expect-error CSSTree types are not updated to v3 yet
list.push([layer.name])
for (let layer_name of get_layer_names(layer)) {
root.add_child(current_stack, layer_name, location)
current_stack.push(layer_name)
}
return this.skip
}

// @import url("foo.css") layer();
let layer_fn = csstree.find(
node.prelude,
(pr_node) =>
pr_node.type === 'Function' && pr_node.name.toLowerCase() === 'layer'
)
let layer_fn = csstree.find(prelude, n => n.type === 'Function' && n.name.toLowerCase() === 'layer')
if (layer_fn) {
list.push(['<anonymous>'])
root.add_child([], get_anonymous_id(), location)
return this.skip
}

// @import url("foo.css") layer;
let layer_keyword = csstree.find(
node.prelude,
(pre_node) =>
pre_node.type === 'Identifier' && pre_node.name.toLowerCase() === 'layer'
)
let layer_keyword = csstree.find(prelude, n => n.type === 'Identifier' && n.name.toLowerCase() === 'layer')
if (layer_keyword) {
list.push(['<anonymous>'])
root.add_child([], get_anonymous_id(), location)
return this.skip
}
}
return this.skip
}
},
leave(node) {
if (is_layer(node)) {
if (node.prelude !== null && node.prelude.type === 'AtrulePrelude') {
let layer_names = get_layer_names(node.prelude)
for (let i = 0; i < layer_names.length; i++) {
current_stack.pop()
}
} else {
// pop the anonymous layer
current_stack.pop()
}
} else if (node.name.toLowerCase() === 'import') {
// clear the stack, imports can not be nested
current_stack.length = 0
}
},
})

return list
return root.to_plain_object().children
}

/**
* @param {string} css
* @returns {LayerTree[]}
*/
export function get_tree(css) {
let ast = csstree.parse(css, {
positions: true,
parseAtrulePrelude: true,
parseRulePrelude: false,
parseValue: false,
parseRulePrelude: false,
parseCustomProperty: false,
})
let list_of_layers = get_ast_tree(ast).map((layer) => layer.join('.'))

let known = new List()

for (let name of list_of_layers) {
if (name.includes('.')) {
let parts = name.split('.')
// @ts-expect-error Let me just do a while loop plz
let last_item = known.push(parts.shift())

while (parts.length > 0 && last_item) {
// @ts-expect-error Let me just do a while loop plz
last_item = last_item.push(parts.shift())
}

continue
}

known.push(name)
}

return known.children.map((child) => child.serialize())
return get_tree_from_ast(ast)
}
Loading