Skip to content
Open
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
234 changes: 125 additions & 109 deletions package-lock.json

Large diffs are not rendered by default.

11 changes: 5 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,17 @@
"scripts": {
"build": "vite build",
"test": "c8 --reporter=lcov uvu",
"check": "tsc",
"check": "tsc --noEmit",
"prettier": "prettier --check src/**/*.js test/**/*.js"
},
"devDependencies": {
"@codecov/vite-plugin": "^1.2.1",
"@types/css-tree": "^2.3.8",
"c8": "^10.1.2",
"prettier": "^3.3.3",
"typescript": "5.4.2",
"uvu": "^0.5.6",
"vite": "^5.4.10"
},
"dependencies": {
"css-tree": "^3.0.0"
},
"files": [
"dist",
"index.d.ts"
Expand All @@ -58,5 +54,8 @@
"useTabs": true,
"printWidth": 140,
"singleQuote": true
},
"dependencies": {
"@projectwallace/css-parser": "^0.6.3"
}
}
}
161 changes: 66 additions & 95 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,49 +1,21 @@
import * as csstree from 'css-tree'
import { TreeNode } from './TreeNode.js'
import { NODE_AT_RULE, NODE_PRELUDE_IMPORT_LAYER, NODE_PRELUDE_LAYER_NAME, parse, traverse } from '@projectwallace/css-parser'

/**
* @typedef Location
* @property {number} line
* @property {number} column
* @property {number} start
* @property {number} end
*/
/** @param {string} name */
function get_layer_names(name) {
return name.split('.').map((s) => s.trim())
}

/**
* @param {import('css-tree').CssNode} node
* @returns {Location | undefined}
*/
function get_location(node) {
let loc = node.loc
if (!loc) return
/** @param {import('@projectwallace/css-parser').CSSNode} node */
function create_location(node) {
return {
line: loc.start.line,
column: loc.start.column,
start: loc.start.offset,
end: loc.end.offset,
line: node.line,
column: node.column,
start: node.offset,
}
}

/** @param {import('css-tree').Atrule} node */
function is_layer(node) {
return node.name.toLowerCase() === 'layer'
}

/**
* @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())
}

/**
* @param {import('css-tree').CssNode} ast
*/
/** @param {import('@projectwallace/css-parser').CSSNode} ast */
export function layer_tree_from_ast(ast) {
/** @type {string[]} */
let current_stack = []
Expand All @@ -56,79 +28,80 @@ export function layer_tree_from_ast(ast) {
return `__anonymous-${anonymous_counter}__`
}

csstree.walk(ast, {
visit: 'Atrule',
traverse(ast, {
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)
} else 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) {
// Split the layer name by dots to handle nested layers
let parts = name.split('.').map((/** @type {string} */ s) => s.trim())
if (node.type !== NODE_AT_RULE) return

if (node.name === 'layer') {
if (node.prelude !== null) {
let groups = node.prelude.split(',').map((s) => s.trim())
if (!node.has_block) {
for (let name of groups) {
let parts = get_layer_names(name)
// Ensure all parent layers exist and add them to the tree
for (let i = 0; i < parts.length; i++) {
let path = parts.slice(0, i)
let layerName = parts[i]
// Only add location to the final layer in dotted notation
// Create a new copy to avoid sharing references
let loc = i === parts.length - 1 ? {...location} : undefined
root.add_child(path, layerName, loc)
let layer_name = parts[i]
if (layer_name) {
// Only add location to the final layer in dotted notation
// Create a new copy to avoid sharing references
let loc = i === parts.length - 1 ? create_location(node) : undefined
root.add_child(path, layer_name, loc)
}
}
}
} else {
for (let layer_name of get_layer_names(node.prelude)) {
root.add_child(current_stack, layer_name, location)
current_stack.push(layer_name)
for (let child of node.children) {
if (child.type === NODE_PRELUDE_LAYER_NAME) {
root.add_child(current_stack, child.text, create_location(node))
current_stack.push(child.text)
}
}
}
} else {
let name = get_anonymous_id()
root.add_child(current_stack, name, create_location(node))
current_stack.push(name)
}
} else if (node.name.toLowerCase() === 'import' && node.prelude !== null && node.prelude.type === 'AtrulePrelude') {
let location = get_location(node)
let prelude = node.prelude

} else if (node.name === 'import') {
// @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(prelude, n => n.type === 'Layer')
if (layer) {
// @ts-expect-error CSSTree types are not updated to v3 yet
for (let layer_name of get_layer_names(layer)) {
root.add_child(current_stack, layer_name, location)
current_stack.push(layer_name)
let layerNode = node.children.find((child) => child.type === NODE_PRELUDE_IMPORT_LAYER)
if (layerNode) {
if (layerNode.name.trim()) {
for (let layer_name of get_layer_names(layerNode.name)) {
root.add_child(current_stack, layer_name, create_location(node))
current_stack.push(layer_name)
}
} else {
// @import url("foo.css") layer;
let name = get_anonymous_id()
root.add_child([], name, create_location(node))
}
return this.skip
}

// @import url("foo.css") layer;
let layer_keyword = csstree.find(prelude, n => n.type === 'Identifier' && n.name.toLowerCase() === 'layer')
if (layer_keyword) {
root.add_child([], get_anonymous_id(), location)
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()
if (node.type !== NODE_AT_RULE) return

if (node.name === 'layer') {
if (node.has_prelude) {
let has_block = node.has_children && node.children.some((c) => c.type !== NODE_PRELUDE_LAYER_NAME)
if (has_block) {
let name = node.children.find((child) => child.type === NODE_PRELUDE_LAYER_NAME)
if (name) {
let layer_names = get_layer_names(name.text)
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') {
} else if (node.name === 'import') {
// clear the stack, imports can not be nested
current_stack.length = 0
}
Expand All @@ -142,13 +115,11 @@ export function layer_tree_from_ast(ast) {
* @param {string} css
*/
export function layer_tree(css) {
let ast = csstree.parse(css, {
positions: true,
parseAtrulePrelude: true,
parseValue: false,
parseRulePrelude: false,
parseCustomProperty: false,
let ast = parse(css, {
parse_selectors: false,
parse_values: false,
skip_comments: true,
})

return layer_tree_from_ast(ast)
}
}
34 changes: 17 additions & 17 deletions test/global.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,29 +26,29 @@ test('mixed imports and layers', () => {
{
name: '__anonymous-1__',
is_anonymous: true,
locations: [{ line: 2, column: 3, start: 3, end: 33 }],
locations: [{ line: 2, column: 3, start: 3 }],
children: [],
},
{
name: 'test',
is_anonymous: false,
locations: [{ line: 3, column: 3, start: 36, end: 72 }],
locations: [{ line: 3, column: 3, start: 36 }],
children: [],
},
{
name: 'anotherTest',
is_anonymous: false,
locations: [{ line: 4, column: 3, start: 75, end: 148 }],
locations: [{ line: 4, column: 3, start: 75 }],
children: [
{
name: 'moreTest',
is_anonymous: false,
locations: [{ line: 5, column: 4, start: 99, end: 144 }],
locations: [{ line: 5, column: 4, start: 99 }],
children: [
{
name: 'deepTest',
is_anonymous: false,
locations: [{ line: 6, column: 5, start: 121, end: 139 }],
locations: [{ line: 6, column: 5, start: 121 }],
children: [],
},
],
Expand All @@ -58,7 +58,7 @@ test('mixed imports and layers', () => {
{
name: '__anonymous-2__',
is_anonymous: true,
locations: [{ line: 10, column: 3, start: 176, end: 185 }],
locations: [{ line: 10, column: 3, start: 176 }],
children: [],
},
]
Expand All @@ -76,70 +76,70 @@ test('the fokus.dev boilerplate', () => {
{
name: 'core',
is_anonymous: false,
locations: [{ line: 2, column: 3, start: 3, end: 49 }],
locations: [{ line: 2, column: 3, start: 3 }],
children: [
{
name: 'reset',
is_anonymous: false,
locations: [{ line: 3, column: 3, start: 52, end: 94 }],
locations: [{ line: 3, column: 3, start: 52 }],
children: [],
},
{
name: 'tokens',
is_anonymous: false,
locations: [{ line: 3, column: 3, start: 52, end: 94 }],
locations: [{ line: 3, column: 3, start: 52 }],
children: [],
},
{
name: 'base',
is_anonymous: false,
locations: [{ line: 3, column: 3, start: 52, end: 94 }],
locations: [{ line: 3, column: 3, start: 52 }],
children: [],
},
],
},
{
name: 'third-party',
is_anonymous: false,
locations: [{ line: 2, column: 3, start: 3, end: 49 }],
locations: [{ line: 2, column: 3, start: 3 }],
children: [
{
name: 'imports',
is_anonymous: false,
locations: [{ line: 4, column: 3, start: 97, end: 147 }],
locations: [{ line: 4, column: 3, start: 97 }],
children: [],
},
{
name: 'overrides',
is_anonymous: false,
locations: [{ line: 4, column: 3, start: 97, end: 147 }],
locations: [{ line: 4, column: 3, start: 97 }],
children: [],
},
],
},
{
name: 'components',
is_anonymous: false,
locations: [{ line: 2, column: 3, start: 3, end: 49 }],
locations: [{ line: 2, column: 3, start: 3 }],
children: [
{
name: 'base',
is_anonymous: false,
locations: [{ line: 5, column: 3, start: 150, end: 196 }],
locations: [{ line: 5, column: 3, start: 150 }],
children: [],
},
{
name: 'variations',
is_anonymous: false,
locations: [{ line: 5, column: 3, start: 150, end: 196 }],
locations: [{ line: 5, column: 3, start: 150 }],
children: [],
},
],
},
{
name: 'utility',
is_anonymous: false,
locations: [{ line: 2, column: 3, start: 3, end: 49 }],
locations: [{ line: 2, column: 3, start: 3 }],
children: [],
},
]
Expand Down
Loading