@node-projects/web-component-designer 0.1.332 → 0.1.334
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +9 -0
- package/dist/elements/documentContainer.js +0 -4
- package/dist/elements/documentContainer.js.map +1 -1
- package/dist/elements/helper/SwitchContainerHelper.js +1 -1
- package/dist/elements/helper/SwitchContainerHelper.js.map +1 -1
- package/dist/elements/services/stylesheetService/AbstractStylesheetService.js +2 -2
- package/dist/elements/services/stylesheetService/AbstractStylesheetService.js.map +1 -1
- package/dist/elements/services/stylesheetService/SpecificityCalculator.d.ts +5 -1
- package/dist/elements/services/stylesheetService/SpecificityCalculator.js +437 -153
- package/dist/elements/services/stylesheetService/SpecificityCalculator.js.map +1 -1
- package/dist/elements/widgets/designerView/IDesignerCanvas.d.ts +4 -0
- package/dist/elements/widgets/designerView/designerCanvas.d.ts +4 -0
- package/dist/elements/widgets/designerView/designerCanvas.js +3 -0
- package/dist/elements/widgets/designerView/designerCanvas.js.map +1 -1
- package/dist/elements/widgets/designerView/extensions/ResizeExtension.js +3 -0
- package/dist/elements/widgets/designerView/extensions/ResizeExtension.js.map +1 -1
- package/dist/elements/widgets/designerView/extensions/contextMenu/ItemsBelowContextMenu.js.map +1 -1
- package/dist/index-min.js +161 -160
- package/dist/index-min.js.map +7 -0
- package/jest.config.js +2 -2
- package/package.json +2 -2
- package/tests/SpecificityCalculator.test.ts +554 -1
package/jest.config.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export default {
|
|
2
2
|
roots: ['<rootDir>/tests'],
|
|
3
|
-
testMatch: ['**/?(*.)+(spec|test).+(ts|tsx|js)'],
|
|
4
|
-
preset: "ts-jest",
|
|
3
|
+
testMatch: ['**/?(*.)+(spec|test).+(mts|ts|tsx|mjs|js)'],
|
|
4
|
+
preset: "ts-jest/presets/default-esm",
|
|
5
5
|
testEnvironment: "node",
|
|
6
6
|
extensionsToTreatAsEsm: ['.ts', '.mts'],
|
|
7
7
|
transform: {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"description": "A WYSIWYG designer webcomponent for html components",
|
|
3
3
|
"name": "@node-projects/web-component-designer",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.334",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"author": "jochen.kuehner@gmx.de",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"link": "npm link",
|
|
14
14
|
"watch": "pm2 start tsc --watch",
|
|
15
15
|
"prepublishOnly": "npm run build && npm run bundle",
|
|
16
|
-
"bundle": "esbuild ./dist/index-all.js --format=esm --minify --external:@node-projects/base-custom-webcomponent --platform=neutral --bundle --outfile=./dist/index-min.js"
|
|
16
|
+
"bundle": "esbuild ./dist/index-all.js --format=esm --minify --sourcemap --external:@node-projects/base-custom-webcomponent --platform=neutral --bundle --outfile=./dist/index-min.js"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"@node-projects/base-custom-webcomponent": ">=0.27.8"
|
|
@@ -69,4 +69,557 @@ test('test 10', () => {
|
|
|
69
69
|
expect(res.A).toBe(1);
|
|
70
70
|
expect(res.B).toBe(3);
|
|
71
71
|
expect(res.C).toBe(1);
|
|
72
|
-
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('test 11', () => {
|
|
75
|
+
const res = calculateSpecificity('button:not(:nth-child(2))');
|
|
76
|
+
expect(res.A).toBe(0);
|
|
77
|
+
expect(res.B).toBe(1);
|
|
78
|
+
expect(res.C).toBe(1);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('test 12', () => {
|
|
82
|
+
const res = calculateSpecificity(':nth-child(2 of .a, #b)');
|
|
83
|
+
expect(res.A).toBe(1);
|
|
84
|
+
expect(res.B).toBe(1);
|
|
85
|
+
expect(res.C).toBe(0);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('test 13', () => {
|
|
89
|
+
const res = calculateSpecificity(':where(#id, .class)');
|
|
90
|
+
expect(res.A).toBe(0);
|
|
91
|
+
expect(res.B).toBe(0);
|
|
92
|
+
expect(res.C).toBe(0);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('test 14 - escaped class', () => {
|
|
96
|
+
const res = calculateSpecificity('.\\31 23');
|
|
97
|
+
expect(res.A).toBe(0);
|
|
98
|
+
expect(res.B).toBe(1);
|
|
99
|
+
expect(res.C).toBe(0);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('test 15 - escaped id', () => {
|
|
103
|
+
const res = calculateSpecificity('#\\#id');
|
|
104
|
+
expect(res.A).toBe(1);
|
|
105
|
+
expect(res.B).toBe(0);
|
|
106
|
+
expect(res.C).toBe(0);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('test 16 - attribute with closing bracket in string', () => {
|
|
110
|
+
const res = calculateSpecificity('[data="a]b"]');
|
|
111
|
+
expect(res.A).toBe(0);
|
|
112
|
+
expect(res.B).toBe(1);
|
|
113
|
+
expect(res.C).toBe(0);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('test 17 - attribute with parentheses in string', () => {
|
|
117
|
+
const res = calculateSpecificity('[data="(test)"]');
|
|
118
|
+
expect(res.A).toBe(0);
|
|
119
|
+
expect(res.B).toBe(1);
|
|
120
|
+
expect(res.C).toBe(0);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('test 18 - nth-child with of class', () => {
|
|
124
|
+
const res = calculateSpecificity(':nth-child(2 of .a)');
|
|
125
|
+
expect(res.A).toBe(0);
|
|
126
|
+
expect(res.B).toBe(2); // nth-child + .a
|
|
127
|
+
expect(res.C).toBe(0);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('test 19 - nth-child with of id', () => {
|
|
131
|
+
const res = calculateSpecificity(':nth-child(2 of #a)');
|
|
132
|
+
expect(res.A).toBe(1);
|
|
133
|
+
expect(res.B).toBe(1);
|
|
134
|
+
expect(res.C).toBe(0);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('test 20 - nth-child with selector list', () => {
|
|
138
|
+
const res = calculateSpecificity(':nth-child(2 of .a, #b)');
|
|
139
|
+
expect(res.A).toBe(1); // max(#b)
|
|
140
|
+
expect(res.B).toBe(1); // nth-child
|
|
141
|
+
expect(res.C).toBe(0);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('test 21 - nth-child with whitespace', () => {
|
|
145
|
+
const res = calculateSpecificity(':nth-child(2 of .a)');
|
|
146
|
+
expect(res.A).toBe(0);
|
|
147
|
+
expect(res.B).toBe(2);
|
|
148
|
+
expect(res.C).toBe(0);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('test 22 - nth-child without of', () => {
|
|
152
|
+
const res = calculateSpecificity(':nth-child(2)');
|
|
153
|
+
expect(res.A).toBe(0);
|
|
154
|
+
expect(res.B).toBe(1);
|
|
155
|
+
expect(res.C).toBe(0);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('test 23 - nested strings and brackets', () => {
|
|
159
|
+
const res = calculateSpecificity('[data="a(b[c]d)e"]');
|
|
160
|
+
expect(res.A).toBe(0);
|
|
161
|
+
expect(res.B).toBe(1);
|
|
162
|
+
expect(res.C).toBe(0);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('test 24 - namespaced element', () => {
|
|
166
|
+
const res = calculateSpecificity('svg|rect');
|
|
167
|
+
expect(res.A).toBe(0);
|
|
168
|
+
expect(res.B).toBe(0);
|
|
169
|
+
expect(res.C).toBe(1);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test('test 25 - universal with namespace', () => {
|
|
173
|
+
const res = calculateSpecificity('*|div');
|
|
174
|
+
expect(res.A).toBe(0);
|
|
175
|
+
expect(res.B).toBe(0);
|
|
176
|
+
expect(res.C).toBe(1);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test('test 26 - empty namespace', () => {
|
|
180
|
+
const res = calculateSpecificity('|div');
|
|
181
|
+
expect(res.A).toBe(0);
|
|
182
|
+
expect(res.B).toBe(0);
|
|
183
|
+
expect(res.C).toBe(1);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test('test 27 - pseudo-element with args ::part', () => {
|
|
187
|
+
const res = calculateSpecificity('::part(button)');
|
|
188
|
+
expect(res.A).toBe(0);
|
|
189
|
+
expect(res.B).toBe(0);
|
|
190
|
+
expect(res.C).toBe(1);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test('test 28 - pseudo-element with args ::slotted', () => {
|
|
194
|
+
const res = calculateSpecificity('::slotted(span)');
|
|
195
|
+
expect(res.A).toBe(0);
|
|
196
|
+
expect(res.B).toBe(0);
|
|
197
|
+
expect(res.C).toBe(1);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('test 29 - combined namespace + pseudo-element', () => {
|
|
201
|
+
const res = calculateSpecificity('svg|rect::part(foo)');
|
|
202
|
+
expect(res.A).toBe(0);
|
|
203
|
+
expect(res.B).toBe(0);
|
|
204
|
+
expect(res.C).toBe(2); // rect + ::part
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test('test 30 - multiple pseudo-elements', () => {
|
|
208
|
+
const res = calculateSpecificity('::slotted(span)::part(button)');
|
|
209
|
+
expect(res.A).toBe(0);
|
|
210
|
+
expect(res.B).toBe(0);
|
|
211
|
+
expect(res.C).toBe(2);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test('test 31 - multiple pseudo-classes', () => {
|
|
215
|
+
const res = calculateSpecificity('div.class1.class2:hover:focus');
|
|
216
|
+
expect(res.A).toBe(0); // no ID
|
|
217
|
+
expect(res.B).toBe(4); // 2 classes + :hover + :focus
|
|
218
|
+
expect(res.C).toBe(1); // div type selector
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test('test 32 - pseudo-element and class', () => {
|
|
222
|
+
const res = calculateSpecificity('p::before.highlight');
|
|
223
|
+
expect(res.A).toBe(0);
|
|
224
|
+
expect(res.B).toBe(1); // .highlight
|
|
225
|
+
expect(res.C).toBe(2); // p + ::before
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test('test 33 - nested :is() and :not()', () => {
|
|
229
|
+
const res = calculateSpecificity(':is(.a, #b):not(.c)');
|
|
230
|
+
expect(res.A).toBe(1); // max of #b
|
|
231
|
+
expect(res.B).toBe(1); // .c
|
|
232
|
+
expect(res.C).toBe(0);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test('test 34 - complex descendant combinators', () => {
|
|
236
|
+
const res = calculateSpecificity('ul li .item > a#link:hover');
|
|
237
|
+
expect(res.A).toBe(1); // #link
|
|
238
|
+
expect(res.B).toBe(2); // .item + :hover
|
|
239
|
+
expect(res.C).toBe(3); // ul + li + a
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test('test 35 - attribute selectors', () => {
|
|
243
|
+
const res = calculateSpecificity('[data-id="123"].active');
|
|
244
|
+
expect(res.A).toBe(0);
|
|
245
|
+
expect(res.B).toBe(2); // [attr] + .active
|
|
246
|
+
expect(res.C).toBe(0);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test('test 36 - multiple pseudo-elements', () => {
|
|
250
|
+
const res = calculateSpecificity('div::first-line::after');
|
|
251
|
+
expect(res.A).toBe(0);
|
|
252
|
+
expect(res.B).toBe(0);
|
|
253
|
+
expect(res.C).toBe(3); // div + ::first-line + ::after
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test('test 37 - universal selector with class', () => {
|
|
257
|
+
const res = calculateSpecificity('*[role="button"].btn');
|
|
258
|
+
expect(res.A).toBe(0);
|
|
259
|
+
expect(res.B).toBe(2); // [role] + .btn
|
|
260
|
+
expect(res.C).toBe(0); // universal selector doesn't count
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test('test 38 - nested :has()', () => {
|
|
264
|
+
const res = calculateSpecificity('div:has(> span.highlight, a#link)');
|
|
265
|
+
expect(res.A).toBe(1); // #link (most specific argument: a#link)
|
|
266
|
+
expect(res.B).toBe(0);
|
|
267
|
+
expect(res.C).toBe(2); // div + a
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test('test 39 - :where() does not increase specificity', () => {
|
|
271
|
+
const res = calculateSpecificity(':where(.a, #b)');
|
|
272
|
+
expect(res.A).toBe(0);
|
|
273
|
+
expect(res.B).toBe(0);
|
|
274
|
+
expect(res.C).toBe(0);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test('test 40 - type selector and namespace', () => {
|
|
278
|
+
const res = calculateSpecificity('svg|circle.special');
|
|
279
|
+
expect(res.A).toBe(0);
|
|
280
|
+
expect(res.B).toBe(1); // .special
|
|
281
|
+
expect(res.C).toBe(1); // circle type
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test('test 41 - multiple combinators and pseudo-elements', () => {
|
|
285
|
+
const res = calculateSpecificity('header nav > ul li::after');
|
|
286
|
+
expect(res.A).toBe(0);
|
|
287
|
+
expect(res.B).toBe(0);
|
|
288
|
+
expect(res.C).toBe(5); // header + nav + ul + li + ::after
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test('test 42 - :nth-last-child with of selector list', () => {
|
|
292
|
+
const res = calculateSpecificity(':nth-last-child(3 of .x, #y, a)');
|
|
293
|
+
expect(res.A).toBe(1); // max from #y
|
|
294
|
+
expect(res.B).toBe(1); // nth-last-child itself
|
|
295
|
+
expect(res.C).toBe(0);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test('test 43 - escaping in identifiers', () => {
|
|
299
|
+
const res = calculateSpecificity('.class\\#escaped #id\\:special');
|
|
300
|
+
expect(res.A).toBe(1); // #id\:special
|
|
301
|
+
expect(res.B).toBe(1); // .class\#escaped
|
|
302
|
+
expect(res.C).toBe(0);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test('test 44 - nested functional pseudo-classes', () => {
|
|
306
|
+
const res = calculateSpecificity(':is(:not(.a), :has(#b))');
|
|
307
|
+
expect(res.A).toBe(1); // #b (most specific :is arg: :has(#b) = (1,0,0))
|
|
308
|
+
expect(res.B).toBe(0);
|
|
309
|
+
expect(res.C).toBe(0);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
test('test 45 - complex all together', () => {
|
|
313
|
+
const res = calculateSpecificity('body > header.navbar :is(ul li:first-child, a#link.active):hover');
|
|
314
|
+
expect(res.A).toBe(1); // #link (most specific :is arg: a#link.active)
|
|
315
|
+
expect(res.B).toBe(3); // .navbar + .active + :hover
|
|
316
|
+
expect(res.C).toBe(3); // body + header + a
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test('test 46 - deeply nested :is() and :not()', () => {
|
|
320
|
+
const res = calculateSpecificity(':is(:not(.a, #b), .c)');
|
|
321
|
+
expect(res.A).toBe(1); // #b (most specific :is arg: :not(.a, #b) = (1,0,0))
|
|
322
|
+
expect(res.B).toBe(0);
|
|
323
|
+
expect(res.C).toBe(0);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test('test 47 - multiple combinators with pseudo-classes', () => {
|
|
327
|
+
const res = calculateSpecificity('div > ul li:first-child.active + a:hover');
|
|
328
|
+
expect(res.A).toBe(0);
|
|
329
|
+
expect(res.B).toBe(3); // .active + :first-child + :hover
|
|
330
|
+
expect(res.C).toBe(4); // div + ul + li + a
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
test('test 48 - :slotted pseudo-class', () => {
|
|
334
|
+
const res = calculateSpecificity(':slotted(.item#id)');
|
|
335
|
+
expect(res.A).toBe(1); // #id
|
|
336
|
+
expect(res.B).toBe(1); // .item
|
|
337
|
+
expect(res.C).toBe(0);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test('test 49 - :host() pseudo-class', () => {
|
|
341
|
+
const res = calculateSpecificity(':host(.container)');
|
|
342
|
+
expect(res.A).toBe(0);
|
|
343
|
+
expect(res.B).toBe(2); // :host pseudo-class + .container
|
|
344
|
+
expect(res.C).toBe(0);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
test('test 50 - :host-context() pseudo-class', () => {
|
|
348
|
+
const res = calculateSpecificity(':host-context(#parent) .child');
|
|
349
|
+
expect(res.A).toBe(1); // #parent
|
|
350
|
+
expect(res.B).toBe(2); // :host-context pseudo-class + .child
|
|
351
|
+
expect(res.C).toBe(0);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
test('test 51 - multiple pseudo-elements in chain', () => {
|
|
355
|
+
const res = calculateSpecificity('div::first-letter::after');
|
|
356
|
+
expect(res.A).toBe(0);
|
|
357
|
+
expect(res.B).toBe(0);
|
|
358
|
+
expect(res.C).toBe(3); // div + ::first-letter + ::after
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
test('test 52 - universal + class + pseudo', () => {
|
|
362
|
+
const res = calculateSpecificity('*:hover.active');
|
|
363
|
+
expect(res.A).toBe(0);
|
|
364
|
+
expect(res.B).toBe(2); // .active + :hover
|
|
365
|
+
expect(res.C).toBe(0); // * doesn't count
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
test('test 53 - type + class + attribute', () => {
|
|
369
|
+
const res = calculateSpecificity('button.btn[type="submit"]');
|
|
370
|
+
expect(res.A).toBe(0);
|
|
371
|
+
expect(res.B).toBe(2); // .btn + [type]
|
|
372
|
+
expect(res.C).toBe(1); // button
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
test('test 54 - deeply nested :has()', () => {
|
|
376
|
+
const res = calculateSpecificity('div:has(ul li:first-child, a#link)');
|
|
377
|
+
expect(res.A).toBe(1); // #link (most specific argument: a#link)
|
|
378
|
+
expect(res.B).toBe(0);
|
|
379
|
+
expect(res.C).toBe(2); // div + a
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
test('test 55 - multiple :where()', () => {
|
|
383
|
+
const res = calculateSpecificity(':where(.a, #b):where(.c, div)');
|
|
384
|
+
expect(res.A).toBe(0);
|
|
385
|
+
expect(res.B).toBe(0);
|
|
386
|
+
expect(res.C).toBe(0);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
test('test 56 - type selector + namespace', () => {
|
|
390
|
+
const res = calculateSpecificity('html|body main|article.section');
|
|
391
|
+
expect(res.A).toBe(0);
|
|
392
|
+
expect(res.B).toBe(1); // .section
|
|
393
|
+
expect(res.C).toBe(2); // body + article (namespaced type selectors)
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
test('test 57 - multiple descendant combinators', () => {
|
|
397
|
+
const res = calculateSpecificity('header nav ul li a.link:hover');
|
|
398
|
+
expect(res.A).toBe(0);
|
|
399
|
+
expect(res.B).toBe(2); // .link + :hover
|
|
400
|
+
expect(res.C).toBe(5); // header + nav + ul + li + a
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
test('test 58 - :not() with type + class', () => {
|
|
404
|
+
const res = calculateSpecificity(':not(div.item)');
|
|
405
|
+
expect(res.A).toBe(0);
|
|
406
|
+
expect(res.B).toBe(1); // .item
|
|
407
|
+
expect(res.C).toBe(1); // div
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
test('test 59 - :is() inside :has()', () => {
|
|
411
|
+
const res = calculateSpecificity('section:has(:is(.a, #b))');
|
|
412
|
+
expect(res.A).toBe(1); // #b (most specific :is arg: #b = (1,0,0))
|
|
413
|
+
expect(res.B).toBe(0);
|
|
414
|
+
expect(res.C).toBe(1); // section
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
test('test 60 - multiple functional pseudo-classes', () => {
|
|
418
|
+
const res = calculateSpecificity(':not(:is(.a, #b)):has(.c)');
|
|
419
|
+
expect(res.A).toBe(1); // #b (most specific :is arg: #b = (1,0,0))
|
|
420
|
+
expect(res.B).toBe(1); // .c from :has()
|
|
421
|
+
expect(res.C).toBe(0);
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
// --- Legacy single-colon pseudo-elements ---
|
|
425
|
+
|
|
426
|
+
test('test 61 - legacy :before pseudo-element', () => {
|
|
427
|
+
const res = calculateSpecificity('p:before');
|
|
428
|
+
expect(res.A).toBe(0);
|
|
429
|
+
expect(res.B).toBe(0);
|
|
430
|
+
expect(res.C).toBe(2); // p + :before (pseudo-element)
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
test('test 62 - legacy :after pseudo-element', () => {
|
|
434
|
+
const res = calculateSpecificity('div.item:after');
|
|
435
|
+
expect(res.A).toBe(0);
|
|
436
|
+
expect(res.B).toBe(1); // .item
|
|
437
|
+
expect(res.C).toBe(2); // div + :after
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
test('test 63 - legacy :first-line pseudo-element', () => {
|
|
441
|
+
const res = calculateSpecificity('p:first-line');
|
|
442
|
+
expect(res.A).toBe(0);
|
|
443
|
+
expect(res.B).toBe(0);
|
|
444
|
+
expect(res.C).toBe(2); // p + :first-line
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
test('test 64 - legacy :first-letter pseudo-element', () => {
|
|
448
|
+
const res = calculateSpecificity('p:first-letter');
|
|
449
|
+
expect(res.A).toBe(0);
|
|
450
|
+
expect(res.B).toBe(0);
|
|
451
|
+
expect(res.C).toBe(2); // p + :first-letter
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
// --- :matches() and vendor-prefixed :any() ---
|
|
455
|
+
|
|
456
|
+
test('test 65 - :matches() behaves like :is()', () => {
|
|
457
|
+
const res = calculateSpecificity(':matches(.a, #b)');
|
|
458
|
+
expect(res.A).toBe(1); // most specific arg: #b
|
|
459
|
+
expect(res.B).toBe(0);
|
|
460
|
+
expect(res.C).toBe(0);
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
test('test 66 - :-webkit-any() behaves like :is()', () => {
|
|
464
|
+
const res = calculateSpecificity(':-webkit-any(.a, #b)');
|
|
465
|
+
expect(res.A).toBe(1);
|
|
466
|
+
expect(res.B).toBe(0);
|
|
467
|
+
expect(res.C).toBe(0);
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
test('test 67 - :-moz-any() behaves like :is()', () => {
|
|
471
|
+
const res = calculateSpecificity(':-moz-any(.a, #b)');
|
|
472
|
+
expect(res.A).toBe(1);
|
|
473
|
+
expect(res.B).toBe(0);
|
|
474
|
+
expect(res.C).toBe(0);
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
// --- Column combinator || ---
|
|
478
|
+
|
|
479
|
+
test('test 68 - column combinator ||', () => {
|
|
480
|
+
const res = calculateSpecificity('col.selected || td');
|
|
481
|
+
expect(res.A).toBe(0);
|
|
482
|
+
expect(res.B).toBe(1); // .selected
|
|
483
|
+
expect(res.C).toBe(2); // col + td
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
// --- Nesting selector & ---
|
|
487
|
+
|
|
488
|
+
test('test 69 - bare nesting selector & has zero specificity', () => {
|
|
489
|
+
const res = calculateSpecificity('&');
|
|
490
|
+
expect(res.A).toBe(0);
|
|
491
|
+
expect(res.B).toBe(0);
|
|
492
|
+
expect(res.C).toBe(0);
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
test('test 70 - & with class', () => {
|
|
496
|
+
const res = calculateSpecificity('&.active');
|
|
497
|
+
expect(res.A).toBe(0);
|
|
498
|
+
expect(res.B).toBe(1); // .active
|
|
499
|
+
expect(res.C).toBe(0); // & has zero specificity on its own
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
test('test 71 - & as descendant', () => {
|
|
503
|
+
const res = calculateSpecificity('& .child');
|
|
504
|
+
expect(res.A).toBe(0);
|
|
505
|
+
expect(res.B).toBe(1); // .child
|
|
506
|
+
expect(res.C).toBe(0); // & has zero specificity on its own
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
test('test 72 - & with pseudo-class', () => {
|
|
510
|
+
const res = calculateSpecificity('&:hover');
|
|
511
|
+
expect(res.A).toBe(0);
|
|
512
|
+
expect(res.B).toBe(1); // :hover
|
|
513
|
+
expect(res.C).toBe(0); // & has zero specificity on its own
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
test('test 73 - & with pseudo-element', () => {
|
|
517
|
+
const res = calculateSpecificity('&::before');
|
|
518
|
+
expect(res.A).toBe(0);
|
|
519
|
+
expect(res.B).toBe(0);
|
|
520
|
+
expect(res.C).toBe(1); // ::before
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
// --- Performance test ---
|
|
524
|
+
|
|
525
|
+
test('performance - 100k iterations across selector categories', () => {
|
|
526
|
+
const selectors = {
|
|
527
|
+
simple: [
|
|
528
|
+
'div',
|
|
529
|
+
'.btn',
|
|
530
|
+
'#main',
|
|
531
|
+
'div.container',
|
|
532
|
+
'ul li a.link',
|
|
533
|
+
'body > header nav ul li',
|
|
534
|
+
'#app .sidebar .nav-item',
|
|
535
|
+
'main > section article p span',
|
|
536
|
+
],
|
|
537
|
+
withAttributes: [
|
|
538
|
+
'[data-id]',
|
|
539
|
+
'input[type="text"]',
|
|
540
|
+
'a[href^="https"][target="_blank"].external',
|
|
541
|
+
'div[class~="active"][role="button"]',
|
|
542
|
+
],
|
|
543
|
+
withPseudoClasses: [
|
|
544
|
+
'a:hover',
|
|
545
|
+
'div:first-child',
|
|
546
|
+
'li:nth-child(2n+1)',
|
|
547
|
+
'input:focus:not(:disabled)',
|
|
548
|
+
'tr:nth-child(odd):hover',
|
|
549
|
+
],
|
|
550
|
+
withPseudoElements: [
|
|
551
|
+
'p::before',
|
|
552
|
+
'div::after',
|
|
553
|
+
'p:before',
|
|
554
|
+
'h1::first-line',
|
|
555
|
+
'blockquote::first-letter',
|
|
556
|
+
],
|
|
557
|
+
withIsNotHas: [
|
|
558
|
+
':is(.a, .b, .c)',
|
|
559
|
+
':not(#main)',
|
|
560
|
+
':has(> .child)',
|
|
561
|
+
':is(.nav, #sidebar):not(.hidden)',
|
|
562
|
+
'div:has(> span.highlight, a#link)',
|
|
563
|
+
':is(:not(.a, #b), .c)',
|
|
564
|
+
],
|
|
565
|
+
withWhere: [
|
|
566
|
+
':where(.a, #b)',
|
|
567
|
+
':where(div, span):is(.active)',
|
|
568
|
+
],
|
|
569
|
+
withHostSlotted: [
|
|
570
|
+
':host(.container)',
|
|
571
|
+
':host-context(#parent) .child',
|
|
572
|
+
':slotted(.item#id)',
|
|
573
|
+
],
|
|
574
|
+
complex: [
|
|
575
|
+
'body > header.navbar :is(ul li:first-child, a#link.active):hover',
|
|
576
|
+
':not(:is(.a, #b)):has(.c)',
|
|
577
|
+
'section:has(:is(.a, #b))',
|
|
578
|
+
'div > ul li:first-child.active + a:hover',
|
|
579
|
+
'col.selected || td',
|
|
580
|
+
],
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
const allSelectors = Object.values(selectors).flat();
|
|
584
|
+
const iterations = 100_000;
|
|
585
|
+
|
|
586
|
+
// Warmup
|
|
587
|
+
for (let i = 0; i < 1000; i++) {
|
|
588
|
+
for (const sel of allSelectors) calculateSpecificity(sel);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const categoryResults: Record<string, { ops: number; nsPerOp: number }> = {};
|
|
592
|
+
|
|
593
|
+
// Benchmark per category
|
|
594
|
+
for (const [category, sels] of Object.entries(selectors)) {
|
|
595
|
+
const start = performance.now();
|
|
596
|
+
for (let i = 0; i < iterations; i++) {
|
|
597
|
+
for (const sel of sels) calculateSpecificity(sel);
|
|
598
|
+
}
|
|
599
|
+
const elapsed = performance.now() - start;
|
|
600
|
+
const totalOps = iterations * sels.length;
|
|
601
|
+
categoryResults[category] = {
|
|
602
|
+
ops: totalOps,
|
|
603
|
+
nsPerOp: (elapsed * 1_000_000) / totalOps,
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Overall benchmark
|
|
608
|
+
const overallStart = performance.now();
|
|
609
|
+
for (let i = 0; i < iterations; i++) {
|
|
610
|
+
for (const sel of allSelectors) calculateSpecificity(sel);
|
|
611
|
+
}
|
|
612
|
+
const overallElapsed = performance.now() - overallStart;
|
|
613
|
+
const totalOps = iterations * allSelectors.length;
|
|
614
|
+
|
|
615
|
+
// Print results
|
|
616
|
+
console.log('\n── Specificity Calculator Performance ──');
|
|
617
|
+
console.log(`Total: ${totalOps.toLocaleString()} ops in ${overallElapsed.toFixed(1)}ms (${((overallElapsed * 1_000_000) / totalOps).toFixed(0)}ns/op)\n`);
|
|
618
|
+
for (const [category, result] of Object.entries(categoryResults)) {
|
|
619
|
+
console.log(` ${category.padEnd(22)} ${result.nsPerOp.toFixed(0).padStart(5)}ns/op (${result.ops.toLocaleString()} ops)`);
|
|
620
|
+
}
|
|
621
|
+
console.log('');
|
|
622
|
+
|
|
623
|
+
// Sanity check: should complete in reasonable time (< 5 seconds total)
|
|
624
|
+
expect(overallElapsed).toBeLessThan(5000);
|
|
625
|
+
});
|