Skip to content

Commit 5343a0b

Browse files
author
Frank van Viegen
committed
v1.7.2: Consolidated CSS style tags and improved variable handling in parentheses
1 parent bfa4e24 commit 5343a0b

7 files changed

Lines changed: 83 additions & 71 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
### 1.7.2 (2026-01-28)
4+
5+
**Fixes:**
6+
- `insertCss` and `insertGlobalCss` no longer litter the DOM with style-tags, instead maintaining just a single tag for all inserted CSS.
7+
- The `$` CSS variable shorthand now works within parenthesis, e.g. `width: calc(100% - $sidebarWidth);`, but not within `url( ... )`.
8+
39
### 1.7.1 (2026-01-27)
410

511
**Fixes:**

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "aberdeen",
3-
"version": "1.7.1",
3+
"version": "1.7.2",
44
"author": "Frank van Viegen",
55
"main": "dist-min/aberdeen.js",
66
"devDependencies": {

‎src/aberdeen.ts‎

Lines changed: 39 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1803,8 +1803,8 @@ export function setSpacingCssVars(base = 1, unit = 'rem'): void {
18031803
}
18041804
}
18051805

1806-
// Matches: (1) parenthesized content, (2) quoted content, (3) $varName at start or after space
1807-
const CSS_VAR_PATTERN = /(\([^)]*\))|("[^"]*")|(^| )\$(\w+)/g;
1806+
// Matches: (1) url() content, (2) quoted content, (3) $varName at start or after space
1807+
const CSS_VAR_PATTERN = /(\burl\([^)]*\))|("[^"]*")|(^| )\$(\w+)/g;
18081808
const DIGIT_FIRST = /^\d/;
18091809

18101810
/**
@@ -1822,28 +1822,6 @@ function cssVarRef(value: string): string {
18221822
});
18231823
}
18241824

1825-
// Automatically mount cssVars style tag to document.head when cssVars is not empty
1826-
if (typeof document !== "undefined") {
1827-
leakScope(() => {
1828-
$(() => {
1829-
if (!isEmpty(cssVars)) {
1830-
mount(document.head, () => {
1831-
$('style', () => {
1832-
let css = ":root {\n";
1833-
for(const [key, value] of Object.entries(cssVars)) {
1834-
const varName = DIGIT_FIRST.test(String(key)) ? `m${key}` : key;
1835-
css += ` --${varName}: ${value};\n`;
1836-
}
1837-
css += "}";
1838-
$(`#${css}`);
1839-
});
1840-
});
1841-
}
1842-
});
1843-
});
1844-
}
1845-
1846-
18471825
let darkModeState: {value: boolean} | undefined;
18481826

18491827
/**
@@ -2349,9 +2327,15 @@ let cssCount = 0;
23492327
export function insertCss(style: string | object): string {
23502328
const prefix = `.AbdStl${++cssCount}`;
23512329
const css = typeof style === 'string' ? styleStringToCss(style, prefix) : objectToCss(style, prefix);
2352-
if (css) $(`style#${css}`);
2330+
if (css) {
2331+
let cnt = cssSnippetCount++;
2332+
cssSnippets[cnt] = css;
2333+
clean(() => delete cssSnippets[cnt]);
2334+
}
23532335
return prefix;
23542336
}
2337+
let cssSnippets = proxy({} as Record<number, string>);
2338+
let cssSnippetCount = 0;
23552339

23562340
function combinePrefixSelector(prefix: string, key: string): string {
23572341
const sel = [];
@@ -2478,7 +2462,11 @@ function styleStringToCss(styleStr: string, selector: string): string {
24782462
*/
24792463
export function insertGlobalCss(style: object) {
24802464
const css = objectToCss(style, "");
2481-
if (css) $(`style#${css}`);
2465+
if (css) {
2466+
let cnt = cssSnippetCount++;
2467+
cssSnippets[cnt] = css;
2468+
clean(() => delete cssSnippets[cnt]);
2469+
}
24822470
}
24832471

24842472
const CSS_SHORT: Record<string, string | string[]> = {
@@ -3183,3 +3171,28 @@ export function withEmitHandler(
31833171
}
31843172

31853173

3174+
// Automatically add cssVars and cssSnippets (from insertCss) into a <head> style tag
3175+
if (typeof document !== "undefined") {
3176+
leakScope(() => {
3177+
$(() => {
3178+
if (isEmpty(cssSnippets) && isEmpty(cssVars)) return;
3179+
mount(document.head, () => {
3180+
$('style.abd', () => {
3181+
onEach(cssSnippets, (value) => {
3182+
$('#', value);
3183+
});
3184+
$(() => {
3185+
if (isEmpty(cssVars)) return;
3186+
let css = ":root{";
3187+
for(const [key, value] of Object.entries(cssVars)) {
3188+
const varName = DIGIT_FIRST.test(String(key)) ? `m${key}` : key;
3189+
css += `--${varName}:${value};`;
3190+
}
3191+
css += "}\n";
3192+
$('#', css);
3193+
})
3194+
});
3195+
});
3196+
});
3197+
});
3198+
}

‎tests/cssShortcuts.test.ts‎

Lines changed: 13 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,7 @@
11
import { test, expect } from "bun:test";
2-
import { assertBody, assertCss, passTime } from "./helpers";
2+
import { assertBody, getCss, assertCss, passTime } from "./helpers";
33
import { $, cssVars, setSpacingCssVars, insertCss } from "../src/aberdeen";
44

5-
/** Get the full CSS content from the :root style tag in head */
6-
function getHeadCss(): string {
7-
let css = '';
8-
(document.head as any).visit((el: Element) => {
9-
if (el.tagName === 'style') {
10-
css += el.textContent;
11-
}
12-
});
13-
return css;
14-
}
15-
165
// Property shortcuts
176
test('margin shortcuts', () => {
187
$('div m:10px'); assertBody(`div{margin:10px}`);
@@ -85,27 +74,26 @@ test('cssVars automatically creates :root style tag when not empty', async () =>
8574
await passTime();
8675

8776
// Check that the spacing vars are in :root (auto-mounted because cssVars is not empty)
88-
const css = getHeadCss();
89-
expect(css).toContain('--m3: 1rem;');
90-
expect(css).toContain('--m4: 2rem;');
77+
const css = getCss();
78+
expect(css).toContain('--m3:1rem;');
79+
expect(css).toContain('--m4:2rem;');
9180
});
9281

9382
test('cssVars changes update :root style tag', async () => {
9483
cssVars.changing = 'red';
95-
await passTime();
9684

9785
// Verify initial value
98-
let css = getHeadCss();
99-
expect(css).toContain('--changing: red');
86+
await passTime();
87+
assertCss(
88+
`:root{--changing:red;}`
89+
);
10090

10191
// Change value
10292
cssVars.changing = 'blue';
10393
await passTime();
104-
105-
// Verify updated value AND old value is gone
106-
css = getHeadCss();
107-
expect(css).toContain('--changing: blue');
108-
expect(css).not.toContain('--changing: red');
94+
assertCss(
95+
`:root{--changing:blue;}`
96+
);
10997
});
11098

11199
// Edge cases
@@ -119,11 +107,12 @@ test('false value clears style', () => {
119107
assertBody(`div`);
120108
});
121109

122-
test('insertCss() supports shortcuts and cssVars', () => {
110+
test('insertCss() supports shortcuts and cssVars', async () => {
123111
const cls = insertCss({
124112
"&": "mv:$3 fg:$primary",
125113
"&:hover": "bg:blue"
126114
});
115+
await passTime();
127116
assertCss(
128117
`${cls}{margin-top:var(--m3);margin-bottom:var(--m3);color:var(--primary);}`,
129118
`${cls}:hover{background:blue;}`

‎tests/fakedom.ts‎

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,6 @@ class Node {
2525
if (idx < 0) throw new Error("not part of siblings!?");
2626
return siblings[idx + delta];
2727
}
28-
29-
visit(visitor: (el: Node) => void) {
30-
visitor(this);
31-
for(let c of this.childNodes) {
32-
c.visit(visitor);
33-
}
34-
}
3528
}
3629

3730
class Element extends Node {

‎tests/helpers.ts‎

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -38,19 +38,19 @@ export function assertBody(expected: string): void {
3838
if (actual !== expected) throw new Error(`assertBody failed\nActual: ${actual}\nExpected: ${expected}`)
3939
}
4040

41-
export function assertCss(...expected: string[]) {
42-
const found: string[] = [];
43-
for (const root of [document.head, document.body]) {
44-
(root as any).visit((el: Element) => {
45-
if (el.tagName === 'style') {
46-
for(let style of el.textContent.trim().split("\n")) {
47-
if (style) {
48-
found.push(style);
49-
}
50-
}
51-
}
52-
});
41+
/** Get the full CSS content from the :root style tag in head */
42+
export function getCss(): string {
43+
let css = '';
44+
for(const el of document.head.childNodes) {
45+
if (el instanceof Element && el.tagName === 'style') {
46+
css += el.textContent + "\n";
47+
}
5348
}
49+
return css;
50+
}
51+
52+
export function assertCss(...expected: string[]) {
53+
const found = getCss().split("\n").map(s => s.trim()).filter(s => s);
5454
expect(found).toEqual(expected);
5555
}
5656

‎tests/insertCss.test.ts‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { test } from "bun:test";
22
import { insertCss, insertGlobalCss } from "../src/aberdeen";
3-
import { assertCss } from './helpers';
3+
import { assertCss, passTime } from './helpers';
44

55
test('Basic style', async () => {
66
let cls = insertCss('color:red');
7+
await passTime();
78
assertCss(`${cls}{color:red;}`);
89
});
910

@@ -15,6 +16,7 @@ test('Complex selectors', async () => {
1516
"body > &": 'margin-bottom:20px',
1617
"body > & span": 'font-size:20',
1718
})
19+
await passTime();
1820
assertCss(
1921
`${cls}{margin:5;}`,
2022
`${cls} .x .y{font-weight:bold;}`,
@@ -29,6 +31,7 @@ test('Global style', async () => {
2931
'*': 'margin:4',
3032
'h1': 'color:red',
3133
});
34+
await passTime();
3235
assertCss(
3336
`*{margin:4;}`,
3437
`h1{color:red;}`,
@@ -44,6 +47,7 @@ test('Nested selectors with object values', async () => {
4447
'&:active': 'transform:scale(0.95)',
4548
},
4649
});
50+
await passTime();
4751
assertCss(
4852
`${cls}{display:flex;}`,
4953
`${cls} button{background:blue;padding:var(--m2);}`,
@@ -61,6 +65,7 @@ test('Media query with object containing multiple selectors', async () => {
6165
'input': 'font-size:16px',
6266
},
6367
});
68+
await passTime();
6469
assertCss(
6570
`${cls}{display:flex;}`,
6671
`@media (max-width: 600px){`,
@@ -76,6 +81,7 @@ test('Media query with string value', async () => {
7681
'&': 'color:black',
7782
'@media (prefers-color-scheme: dark)': 'color:white',
7883
});
84+
await passTime();
7985
assertCss(
8086
`${cls}{color:black;}`,
8187
`@media (prefers-color-scheme: dark){`,
@@ -98,6 +104,7 @@ test('Media queries nested deeply get bubbled to outside', async () => {
98104
},
99105
},
100106
});
107+
await passTime();
101108
assertCss(
102109
`${cls}{display:grid;}`,
103110
`${cls} main{padding:var(--m4);}`,
@@ -114,6 +121,7 @@ test('Quoted values (e.g., content property)', async () => {
114121
'&::before': `content: "★"; color:gold mr:$1`,
115122
'&::after': `content: "\\00A0"; display:inline-block`,
116123
});
124+
await passTime();
117125
assertCss(
118126
`${cls}::before{content:"★";color:gold;margin-right:var(--m1);}`,
119127
`${cls}::after{content:"\\00A0";display:inline-block;}`,
@@ -124,6 +132,7 @@ test('Multi-word values with space-colon-semicolon syntax', async () => {
124132
let cls = insertCss({
125133
'&': `border: 1px solid blue; transition: all 0.3s ease;`,
126134
});
135+
await passTime();
127136
assertCss(
128137
`${cls}{border:1px solid blue;transition:all 0.3s ease;}`,
129138
);
@@ -133,6 +142,7 @@ test('Grid template areas with multiple quoted strings', async () => {
133142
let cls = insertCss({
134143
'&': `grid-template-areas: "header header" "sidebar content" "footer footer";`,
135144
});
145+
await passTime();
136146
assertCss(
137147
`${cls}{grid-template-areas:"header header" "sidebar content" "footer footer";}`,
138148
);
@@ -145,6 +155,7 @@ test('Handles unions combined with further nested selectors', async () => {
145155
'&:hover, &.active': 'color:darkblue'
146156
},
147157
});
158+
await passTime();
148159
assertCss(
149160
`${cls} button,${cls} a.link{color:blue;}`,
150161
`${cls} button:hover,${cls} button.active,${cls} a.link:hover,${cls} a.link.active{color:darkblue;}`,

0 commit comments

Comments
 (0)