Skip to content

Commit 3608c62

Browse files
committed
feat: add comprehensive tests for stylesheet parsing and style resolution
- Implemented nested rules tests in `nesting.test.ts` to validate various nesting scenarios including pseudo-classes, attribute selectors, and theme-based styles. - Created `parser.test.ts` to ensure accurate parsing of stylesheets, covering basic selectors, attribute selectors, pseudo-classes, and sorting rules. - Added performance tests in `performance.test.ts` to evaluate the efficiency of style resolution with a large number of rules and files. - Developed `resolver.test.ts` to verify the correctness of style resolution based on various selectors and attributes. - Introduced tests for the `:root` pseudo-class in `root.test.ts` to confirm its behavior with root folders and specificity. - Added sorting tests in `sorting.test.ts` to ensure correct application of sorting rules. - Implemented theme tests in `theme.test.ts` to validate theme-specific styles and their merging with base styles. - Created TypeScript configuration file `tsconfig.json` for project setup. - Added `tsdown.config.ts` for building and exporting modules.
1 parent d9ca709 commit 3608c62

31 files changed

Lines changed: 4253 additions & 42 deletions
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
ci:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
15+
- uses: pnpm/action-setup@v4
16+
17+
- uses: actions/setup-node@v4
18+
with:
19+
node-version: 22
20+
cache: pnpm
21+
22+
- run: pnpm install --frozen-lockfile
23+
24+
- run: pnpm typecheck
25+
26+
- run: pnpm test -- --run
27+
28+
- run: pnpm build
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: Publish
2+
3+
on:
4+
release:
5+
types: [published]
6+
7+
jobs:
8+
publish:
9+
runs-on: ubuntu-latest
10+
permissions:
11+
contents: read
12+
id-token: write
13+
steps:
14+
- uses: actions/checkout@v4
15+
16+
- uses: pnpm/action-setup@v4
17+
18+
- uses: actions/setup-node@v4
19+
with:
20+
node-version: 24
21+
cache: pnpm
22+
registry-url: https://registry.npmjs.org
23+
24+
- run: pnpm install --frozen-lockfile
25+
26+
- run: pnpm build
27+
28+
- run: pnpm publish --provenance --no-git-checks --access public

packages/fss/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
node_modules
2+
dist
3+
*.log
4+
.DS_Store

packages/fss/.prettierrc.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"singleQuote": true,
3+
"printWidth": 160,
4+
"useTabs": false,
5+
"trailingComma": "all"
6+
}

packages/fss/.vscode/settings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"editor.formatOnSave": true
3+
}

packages/fss/README.md

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
# fss-lang
2+
3+
**File Style Sheets** — a CSS-like language for describing how files should be rendered in a tree or panel.
4+
5+
## Features
6+
7+
- **CSS-like syntax** powered by [css-tree](https://github.com/nicolo-ribaudo/css-tree) for robust parsing
8+
- **Type selectors**: `file`, `folder`
9+
- **Attribute selectors**: `[ext="ts"]`, `[name="Dockerfile"]`, `[lang="typescript"]`, `[vcs-status="modified"]`
10+
- **Attribute operators**: `=`, `^=`, `$=`, `~=`, `!=`
11+
- **Pseudo-classes**: `:expanded`, `:selected`, `:hovered`, `:active`, `:drag-over`, `:focused`
12+
- **`:root` pseudo-class**: `folder:root` matches the top-level folder
13+
- **`:is()` grouping**: `file:is([ext="ts"], [ext="tsx"])`
14+
- **Descendant combinator**: `folder[name=".github"] folder[name="workflows"]`
15+
- **Compound extensions**: `file[ext="test.ts"]` overrides `file[ext="ts"]` — specificity scales by segment count
16+
- **Language selector**: `file[lang="typescript"]` — match by VS Code language ID
17+
- **`@theme` blocks**: `light`, `dark`, `high-contrast`, `high-contrast-light` — like VS Code
18+
- **`@sorting` block** for sort priority/grouping
19+
- **`@table` block** for column visibility/width/order
20+
- **Layered resolution**: global → project → nested `.faraday` overrides
21+
- **Sub-O(N) matching** via rule indexing by name, extension, language, and type
22+
- **Style caching** for amortized O(1) per unique file signature
23+
24+
## Install
25+
26+
```bash
27+
npm install fss-lang
28+
```
29+
30+
## Quick Start
31+
32+
```typescript
33+
import { parseStylesheet, resolveStyle, createFsNode, StateFlags } from 'fss-lang'
34+
35+
// Parse an FSS stylesheet
36+
const sheet = parseStylesheet(`
37+
file { icon: url(default-file.svg); color: white; }
38+
folder { icon: url(default-folder.svg); }
39+
folder:expanded { icon: url(folder-open.svg); }
40+
41+
file[ext="ts"] { icon: url(ts.svg); color: blue; }
42+
file[ext="test.ts"] { icon: url(test.svg); badge: "T"; }
43+
file[name="Dockerfile"] { icon: url(docker.svg); }
44+
45+
file[lang="typescript"] { color: blue; }
46+
file:is([ext="ts"], [ext="tsx"]) { color: blue; }
47+
file[vcs-status="modified"] { color: orange; }
48+
49+
folder:root { icon: url(project-root.svg); }
50+
51+
folder[name=".github"] folder[name="workflows"] {
52+
icon: url(gh-workflow.svg);
53+
}
54+
55+
@sorting {
56+
file[executable] { priority: 10; }
57+
}
58+
59+
@table {
60+
column(size) { visible: false; }
61+
column(vcs-status) { width: 30; order: 2; }
62+
}
63+
`)
64+
65+
// Create a file node
66+
const node = createFsNode({
67+
type: 'file',
68+
name: 'index.ts',
69+
path: '/src/index.ts',
70+
lang: 'typescript',
71+
meta: { 'vcs-status': 'modified' },
72+
})
73+
74+
// Resolve style
75+
const style = resolveStyle(sheet, node, 'dark')
76+
// → { icon: 'url(ts.svg)', color: '#58a6ff' }
77+
```
78+
79+
## Cached Resolution
80+
81+
For large trees (10k+ nodes), use `CachedResolver` to avoid recomputing styles for nodes with identical signatures:
82+
83+
```typescript
84+
import { CachedResolver, parseStylesheet } from 'fss-lang'
85+
86+
const sheet = parseStylesheet(`...`)
87+
const resolver = new CachedResolver(sheet, 'dark')
88+
89+
// Many .ts files share the same style — computed once, cached forever
90+
const style = resolver.resolveStyle(node)
91+
92+
// Switch theme — cache is invalidated automatically
93+
resolver.setTheme('light')
94+
```
95+
96+
## Layer System
97+
98+
Support global config with per-folder overrides (e.g. `.faraday/style.fss`):
99+
100+
```typescript
101+
import { LayeredResolver, createLayer, LayerPriority } from 'fss-lang'
102+
103+
const resolver = new LayeredResolver()
104+
105+
// Global defaults
106+
resolver.addLayer(createLayer(`
107+
file { icon: url(file.svg); }
108+
`, '/', LayerPriority.GLOBAL))
109+
110+
// Project-specific overrides
111+
resolver.addLayer(createLayer(`
112+
file[ext="ts"] { icon: url(custom-ts.svg); }
113+
`, '/my-project/', LayerPriority.PROJECT))
114+
115+
// Nested folder overrides (deeper = higher priority)
116+
resolver.addLayer(createLayer(`
117+
file { icon: url(special.svg); }
118+
`, '/my-project/packages/core/', LayerPriority.nestedPriority(1)))
119+
120+
const style = resolver.resolveStyle(node)
121+
```
122+
123+
## Grammar
124+
125+
### Node Styling
126+
127+
```
128+
file { icon: url(file.svg); }
129+
folder { icon: url(folder.svg); }
130+
file[ext="ts"] { icon: url(ts.svg); }
131+
file[name="Dockerfile"] { icon: url(docker.svg); }
132+
file[ext$="d.ts"] { badge: "DT"; }
133+
folder:expanded { icon: url(open.svg); }
134+
file:is([ext="ts"], [ext="tsx"]) { color: blue; }
135+
file[lang="typescript"] { icon: url(ts.svg); }
136+
folder[name=".github"] folder[name="workflows"] { icon: url(gh.svg); }
137+
file[inVcsRepo] { badge: "V"; }
138+
folder:root { icon: url(project-root.svg); }
139+
```
140+
141+
### Themes
142+
143+
```
144+
@theme dark {
145+
file { color: #ccc; }
146+
file[ext="ts"] { color: #58a6ff; }
147+
}
148+
149+
@theme light {
150+
file { color: #333; }
151+
file[ext="ts"] { color: #0366d6; }
152+
}
153+
154+
@theme high-contrast {
155+
file[ext="ts"] { color: #79c0ff; font-weight: bold; }
156+
}
157+
158+
@theme high-contrast-light {
159+
file[ext="ts"] { color: #0969da; font-weight: bold; }
160+
}
161+
```
162+
163+
Supported kinds: `light`, `dark`, `high-contrast`, `high-contrast-light` (matching VS Code).
164+
Rules outside `@theme` apply in all themes. Theme-scoped rules merge on top.
165+
166+
### Sorting
167+
168+
```
169+
@sorting {
170+
file[executable] { priority: 10; }
171+
folder { group-first: true; }
172+
}
173+
```
174+
175+
### Table Configuration
176+
177+
```
178+
@table {
179+
column(size) { visible: false; }
180+
column(vcs-status) { width: 30; order: 2; }
181+
}
182+
```
183+
184+
## Performance
185+
186+
The engine uses **rule indexing** for sub-O(N) matching:
187+
188+
| Technique | Benefit |
189+
|---|---|
190+
| **Bucket indexing** by name, ext, type | Only evaluate ~5–20 candidate rules instead of all |
191+
| **Bitmask state matching** | Single integer AND for pseudo-class checks |
192+
| **Style key caching** | Amortized O(1) for nodes with identical signatures |
193+
| **Pre-sorted rules** | No sorting during match phase |
194+
195+
| **Candidate caching** | WeakMap-based per-sheet cache for nodes sharing the same bucket key |
196+
197+
For a stylesheet with 10,000 rules and 5,000 files in the same directory, the engine evaluates only a small subset of candidate rules per node. Nodes sharing the same extension/type/name pattern reuse a cached candidate list, and `CachedResolver` further deduplicates resolution for nodes with identical style keys.
198+
199+
## API
200+
201+
### `parseStylesheet(source: string): CompiledStylesheet`
202+
203+
Parse FSS source into an indexed, compiled stylesheet.
204+
205+
### `resolveStyle(sheet: CompiledStylesheet, node: FsNode, theme?: ThemeKind): ResolvedStyle`
206+
207+
Resolve all matching style declarations for a node.
208+
When `theme` is provided, both unscoped and matching-theme rules apply.
209+
210+
### `resolveSorting(sheet: CompiledStylesheet, node: FsNode, theme?: ThemeKind): Record<string, FssValue>`
211+
212+
Resolve sorting declarations for a node.
213+
214+
### `createFsNode(opts): FsNode`
215+
216+
Create a node with auto-computed extensions.
217+
218+
### `CachedResolver`
219+
220+
Style resolver with per-signature caching. Accepts optional `theme` in constructor.
221+
Call `setTheme(theme)` to switch — cache is auto-invalidated.
222+
223+
### `LayeredResolver`
224+
225+
Multi-layer resolver with scope-based priority.
226+
Call `setTheme(theme)` to switch — cache is auto-invalidated.
227+
228+
### `createLayer(source, scopePath, priority): StyleLayer`
229+
230+
Create a scoped style layer from FSS source.
231+
232+
## License
233+
234+
MIT

packages/fss/package.json

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
{
2+
"name": "fss-lang",
3+
"type": "module",
4+
"version": "0.0.5",
5+
"description": "File Style Sheets — a CSS-like language for describing how files should be rendered in a tree or panel.",
6+
"author": "Mikhail Isupov <lam0x86@mail.com>",
7+
"license": "MIT",
8+
"homepage": "https://github.com/dotdirfm/dotdir#readme",
9+
"repository": {
10+
"type": "git",
11+
"url": "git+https://github.com/dotdirfm/dotdir.git"
12+
},
13+
"bugs": {
14+
"url": "https://github.com/dotdirfm/dotdir/issues"
15+
},
16+
"exports": {
17+
".": {
18+
"types": "./dist/index.d.ts",
19+
"import": "./dist/index.js",
20+
"default": "./dist/index.js"
21+
},
22+
"./helpers": {
23+
"types": "./dist/helpers.d.ts",
24+
"import": "./dist/helpers.js",
25+
"default": "./dist/helpers.js"
26+
},
27+
"./package.json": "./package.json"
28+
},
29+
"main": "./dist/index.js",
30+
"module": "./dist/index.js",
31+
"types": "./dist/index.d.ts",
32+
"files": [
33+
"dist"
34+
],
35+
"scripts": {
36+
"build": "tsc -p tsconfig.build.json",
37+
"dev": "tsc -p tsconfig.build.json --watch",
38+
"test": "vitest",
39+
"typecheck": "tsc --noEmit",
40+
"prepublishOnly": "pnpm run build"
41+
},
42+
"devDependencies": {
43+
"@types/css-tree": "^2.3.11",
44+
"@types/node": "^25.0.3",
45+
"typescript": "^5.9.3",
46+
"vitest": "^4.0.16"
47+
},
48+
"dependencies": {
49+
"css-tree": "^3.1.0"
50+
},
51+
"publishConfig": {
52+
"access": "public"
53+
},
54+
"packageManager": "pnpm@10.30.3"
55+
}

0 commit comments

Comments
 (0)