webperf-snippets 0.4.1 → 1.0.0
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/README.md +5 -5
- package/package.json +3 -3
- package/snippets/CoreWebVitals/CLS.js +1 -1
- package/snippets/CoreWebVitals/INP.js +1 -1
- package/snippets/CoreWebVitals/LCP-Image-Entropy.js +15 -1
- package/snippets/CoreWebVitals/LCP-Subparts.js +47 -18
- package/snippets/CoreWebVitals/LCP-Trail.js +35 -10
- package/snippets/CoreWebVitals/LCP-Video-Candidate.js +48 -24
- package/snippets/CoreWebVitals/LCP.js +37 -15
- package/snippets/DevTools-Overrides/Fetch-XHR-Timeline-inject.js +1 -1
- package/snippets/DevTools-Overrides/Fetch-XHR-Timeline-read.js +1 -1
- package/snippets/Interaction/DOM-Size-and-Depth.js +1 -1
- package/snippets/Interaction/Forced-Synchronous-Layout.js +1 -1
- package/snippets/Interaction/Input-Latency-Breakdown.js +1 -1
- package/snippets/Interaction/Interactions.js +1 -1
- package/snippets/Interaction/Layout-Shift-Loading-and-Interaction.js +1 -1
- package/snippets/Interaction/Long-Animation-Frames-Helpers.js +1 -1
- package/snippets/Interaction/Long-Animation-Frames-Script-Attribution.js +1 -1
- package/snippets/Interaction/Long-Animation-Frames.js +1 -1
- package/snippets/Interaction/LongTask.js +1 -1
- package/snippets/Interaction/Scroll-Performance.js +1 -1
- package/snippets/Loading/Back-Forward-Cache.js +19 -2
- package/snippets/Loading/CSS-Media-Queries-Analysis.js +121 -456
- package/snippets/Loading/Cache-Strategy-Analysis.js +41 -20
- package/snippets/Loading/Client-Side-Redirect-Detection.js +11 -3
- package/snippets/Loading/Compression-Audit.js +10 -2
- package/snippets/Loading/Critical-CSS-Detection.js +11 -3
- package/snippets/Loading/Event-Processing-Time.js +1 -1
- package/snippets/Loading/FCP.js +12 -4
- package/snippets/Loading/Find-Above-The-Fold-Lazy-Loaded-Images.js +12 -4
- package/snippets/Loading/Find-Images-With-Lazy-and-Fetchpriority.js +1 -1
- package/snippets/Loading/Find-non-Lazy-Loaded-Images-outside-of-the-viewport.js +10 -2
- package/snippets/Loading/Find-render-blocking-resources.js +11 -3
- package/snippets/Loading/First-And-Third-Party-Script-Info.js +10 -2
- package/snippets/Loading/First-And-Third-Party-Script-Timings.js +10 -2
- package/snippets/Loading/Fonts-Preloaded-Loaded-and-used-above-the-fold.js +1 -1
- package/snippets/Loading/Inline-CSS-Info-and-Size.js +1 -1
- package/snippets/Loading/Inline-Script-Info-and-Size.js +1 -1
- package/snippets/Loading/JS-Execution-Time-Breakdown.js +10 -2
- package/snippets/Loading/Prefetch-Resource-Validation.js +19 -5
- package/snippets/Loading/Priority-Hints-Audit.js +11 -3
- package/snippets/Loading/Resource-Hints-Validation.js +10 -2
- package/snippets/Loading/Resource-Hints.js +11 -3
- package/snippets/Loading/SSR-Hydration-Data-Analysis.js +1 -1
- package/snippets/Loading/Script-Loading.js +10 -2
- package/snippets/Loading/Server-Timing-Early-Hints.js +10 -2
- package/snippets/Loading/Service-Worker-Analysis.js +10 -2
- package/snippets/Loading/Speculation-Rules-Inspector.js +1 -1
- package/snippets/Loading/TTFB-Resources.js +11 -3
- package/snippets/Loading/TTFB-Sub-Parts.js +27 -1
- package/snippets/Loading/TTFB.js +1 -1
- package/snippets/Loading/Third-Party-Impact-by-Domain.js +10 -2
- package/snippets/Loading/Validate-Preload-Async-Defer-Scripts.js +10 -2
- package/snippets/Loading/Webfont-Usage-Analyzer.js +105 -30
- package/snippets/Media/Image-Element-Audit.js +19 -2
- package/snippets/Media/Oversized-Images.js +28 -3
- package/snippets/Media/SVG-Embedded-Bitmap-Analysis.js +48 -9
- package/snippets/Media/Video-Element-Audit.js +16 -10
- package/snippets/README.md +1 -1
- package/snippets/Resources/Network-Bandwidth-Connection-Quality.js +1 -1
- package/snippets/SCHEMA.md +33 -14
- package/snippets/_shared/helpers.js +59 -0
|
@@ -1,7 +1,32 @@
|
|
|
1
1
|
// SVG Embedded Bitmap Analysis
|
|
2
|
-
// https://webperf-snippets.
|
|
2
|
+
// https://webperf-snippets.net
|
|
3
3
|
|
|
4
4
|
(async () => {
|
|
5
|
+
// @shared ownRequestUrl
|
|
6
|
+
// The URL a snippet requests when it reads a file of the page itself (fetch, a HEAD request,
|
|
7
|
+
// new Image()). The fragment never reaches the server or the HTTP cache key, so the file still
|
|
8
|
+
// comes from the cache, but it stays in the name of the Resource Timing entry of the request,
|
|
9
|
+
// which is how isOwnRequest leaves these requests out of what the page loaded.
|
|
10
|
+
function ownRequestUrl(url) {
|
|
11
|
+
try {
|
|
12
|
+
const parsed = new URL(url, location.href);
|
|
13
|
+
if (!/^https?:$/.test(parsed.protocol)) return url;
|
|
14
|
+
parsed.hash = "webperf-snippets";
|
|
15
|
+
return parsed.href;
|
|
16
|
+
} catch {
|
|
17
|
+
return url;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
// @end-shared ownRequestUrl
|
|
21
|
+
|
|
22
|
+
// @shared isOwnRequest
|
|
23
|
+
// True for the Resource Timing entry of a request a snippet made with ownRequestUrl: the page
|
|
24
|
+
// did not make it, so no snippet counts it.
|
|
25
|
+
function isOwnRequest(entry) {
|
|
26
|
+
return entry.name.endsWith("#webperf-snippets");
|
|
27
|
+
}
|
|
28
|
+
// @end-shared isOwnRequest
|
|
29
|
+
|
|
5
30
|
function formatSize(bytes) {
|
|
6
31
|
if (!bytes || bytes === 0) return "—";
|
|
7
32
|
if (bytes < 1024) return `${bytes} B`;
|
|
@@ -55,7 +80,7 @@
|
|
|
55
80
|
|
|
56
81
|
// ── External SVG files (Performance API) ──────────────────────────────
|
|
57
82
|
const svgEntries = performance
|
|
58
|
-
.getEntriesByType("resource")
|
|
83
|
+
.getEntriesByType("resource").filter((e) => !isOwnRequest(e))
|
|
59
84
|
.filter((e) => e.name.split("?")[0].toLowerCase().endsWith(".svg"));
|
|
60
85
|
|
|
61
86
|
const externalResults = await Promise.all(
|
|
@@ -64,7 +89,7 @@
|
|
|
64
89
|
let bitmaps = [];
|
|
65
90
|
|
|
66
91
|
try {
|
|
67
|
-
const res = await fetch(entry.name, { cache: "force-cache" });
|
|
92
|
+
const res = await fetch(ownRequestUrl(entry.name), { cache: "force-cache" });
|
|
68
93
|
const ce = res.headers.get("content-encoding");
|
|
69
94
|
if (ce) compression = ce; // gzip | br | zstd | deflate
|
|
70
95
|
bitmaps = findEmbeddedBitmaps(await res.text());
|
|
@@ -89,6 +114,8 @@
|
|
|
89
114
|
const inlineSvgTotal = inlineSvgs.length;
|
|
90
115
|
const svgsWithUse = inlineSvgs.filter((svg) => svg.querySelector("use")).length;
|
|
91
116
|
const standaloneInlineSvgs = inlineSvgTotal - svgsWithUse;
|
|
117
|
+
// Five or more standalone inline SVGs are worth moving into a <symbol> sprite
|
|
118
|
+
const spriteOpportunity = standaloneInlineSvgs >= 5;
|
|
92
119
|
|
|
93
120
|
const inlineResults = Array.from(document.querySelectorAll("svg"))
|
|
94
121
|
.map((svg, i) => {
|
|
@@ -189,7 +216,7 @@
|
|
|
189
216
|
);
|
|
190
217
|
}
|
|
191
218
|
|
|
192
|
-
if (
|
|
219
|
+
if (spriteOpportunity) {
|
|
193
220
|
console.log("");
|
|
194
221
|
console.group("%c💡 SVG Sprite Opportunity", "color: #3b82f6; font-weight: bold;");
|
|
195
222
|
console.log("");
|
|
@@ -213,6 +240,21 @@
|
|
|
213
240
|
|
|
214
241
|
console.groupEnd();
|
|
215
242
|
|
|
243
|
+
const issues = [];
|
|
244
|
+
if (withBitmaps.length > 0) {
|
|
245
|
+
issues.push({ severity: "warning", message: `${withBitmaps.length} SVG file(s) contain embedded bitmaps` });
|
|
246
|
+
}
|
|
247
|
+
if (spriteOpportunity) {
|
|
248
|
+
const withUse =
|
|
249
|
+
svgsWithUse > 0
|
|
250
|
+
? ` (${svgsWithUse} inline SVG${svgsWithUse === 1 ? " already uses" : "s already use"} <use>)`
|
|
251
|
+
: "";
|
|
252
|
+
issues.push({
|
|
253
|
+
severity: "info",
|
|
254
|
+
message: `${standaloneInlineSvgs} standalone inline SVGs could share one <symbol> sprite referenced with <use>${withUse}`,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
216
258
|
return {
|
|
217
259
|
script: "SVG-Embedded-Bitmap-Analysis",
|
|
218
260
|
status: "ok",
|
|
@@ -222,8 +264,7 @@
|
|
|
222
264
|
inlineSvgTotal,
|
|
223
265
|
svgsWithUse,
|
|
224
266
|
standaloneInlineSvgs,
|
|
225
|
-
|
|
226
|
-
spriteOpportunity: standaloneInlineSvgs >= 5,
|
|
267
|
+
spriteOpportunity,
|
|
227
268
|
},
|
|
228
269
|
items: withBitmaps.map((r) => ({
|
|
229
270
|
url: r.url || r.name,
|
|
@@ -233,8 +274,6 @@
|
|
|
233
274
|
bitmapTypes: r.bitmaps.map((b) => b.format).join(", "),
|
|
234
275
|
sizeBytes: r.transferSize,
|
|
235
276
|
})),
|
|
236
|
-
issues
|
|
237
|
-
? [{ severity: "warning", message: `${withBitmaps.length} SVG file(s) contain embedded bitmaps` }]
|
|
238
|
-
: [],
|
|
277
|
+
issues,
|
|
239
278
|
};
|
|
240
279
|
})();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Video Element Audit
|
|
2
|
-
// https://webperf-snippets.
|
|
2
|
+
// https://webperf-snippets.net
|
|
3
3
|
|
|
4
4
|
(() => {
|
|
5
5
|
function isInViewport(el) {
|
|
@@ -54,8 +54,14 @@
|
|
|
54
54
|
const videos = Array.from(document.querySelectorAll("video"));
|
|
55
55
|
|
|
56
56
|
if (videos.length === 0) {
|
|
57
|
-
console.log("No <video> elements
|
|
58
|
-
return {
|
|
57
|
+
console.log("No <video> elements on this page, nothing to audit.");
|
|
58
|
+
return {
|
|
59
|
+
script: "Video-Element-Audit",
|
|
60
|
+
status: "ok",
|
|
61
|
+
count: 0,
|
|
62
|
+
items: [],
|
|
63
|
+
issues: [{ severity: "info", message: "No <video> elements on this page, so there was nothing to audit." }],
|
|
64
|
+
};
|
|
59
65
|
}
|
|
60
66
|
|
|
61
67
|
const audited = videos.map((video) => {
|
|
@@ -79,35 +85,35 @@
|
|
|
79
85
|
if (preload === "auto" && !autoplay) {
|
|
80
86
|
issues.push({
|
|
81
87
|
s: "error",
|
|
82
|
-
msg: 'preload="auto" loads the full video eagerly
|
|
88
|
+
msg: 'preload="auto" loads the full video eagerly: use preload="none" or preload="metadata"',
|
|
83
89
|
});
|
|
84
90
|
}
|
|
85
91
|
|
|
86
92
|
if (!autoplay && !inViewport && preload !== "none") {
|
|
87
93
|
issues.push({
|
|
88
94
|
s: "warning",
|
|
89
|
-
msg: 'Off-viewport video without preload="none"
|
|
95
|
+
msg: 'Off-viewport video without preload="none": set preload="none" to avoid unnecessary loading',
|
|
90
96
|
});
|
|
91
97
|
}
|
|
92
98
|
|
|
93
99
|
if (autoplay && !muted) {
|
|
94
100
|
issues.push({
|
|
95
101
|
s: "error",
|
|
96
|
-
msg: "autoplay without muted
|
|
102
|
+
msg: "autoplay without muted: browsers block unmuted autoplay",
|
|
97
103
|
});
|
|
98
104
|
}
|
|
99
105
|
|
|
100
106
|
if (autoplay && !playsinline) {
|
|
101
107
|
issues.push({
|
|
102
108
|
s: "warning",
|
|
103
|
-
msg: "autoplay without playsinline
|
|
109
|
+
msg: "autoplay without playsinline: iOS Safari forces fullscreen mode",
|
|
104
110
|
});
|
|
105
111
|
}
|
|
106
112
|
|
|
107
113
|
if (!poster) {
|
|
108
114
|
issues.push({
|
|
109
115
|
s: "warning",
|
|
110
|
-
msg: "Missing poster
|
|
116
|
+
msg: "Missing poster: no preview frame shown before playback starts",
|
|
111
117
|
});
|
|
112
118
|
}
|
|
113
119
|
|
|
@@ -121,14 +127,14 @@
|
|
|
121
127
|
if (!isModernCodec(codec) && codec !== "no source") {
|
|
122
128
|
issues.push({
|
|
123
129
|
s: "info",
|
|
124
|
-
msg: "No modern codec source detected
|
|
130
|
+
msg: "No modern codec source detected: consider adding a WebM source (VP9 or AV1) for better compression",
|
|
125
131
|
});
|
|
126
132
|
}
|
|
127
133
|
|
|
128
134
|
if (!controls && !autoplay) {
|
|
129
135
|
issues.push({
|
|
130
136
|
s: "info",
|
|
131
|
-
msg: "No controls attribute and no autoplay
|
|
137
|
+
msg: "No controls attribute and no autoplay, so users may not be able to play the video",
|
|
132
138
|
});
|
|
133
139
|
}
|
|
134
140
|
|
package/snippets/README.md
CHANGED
|
@@ -87,7 +87,7 @@ See `snippets/Loading/WORKFLOWS.md` for a complete example with:
|
|
|
87
87
|
The `generate-skills.js` script:
|
|
88
88
|
|
|
89
89
|
1. Reads all `.js` files from each category
|
|
90
|
-
2. Extracts metadata from corresponding MDX documentation in `/
|
|
90
|
+
2. Extracts metadata from corresponding MDX documentation in `/content/`
|
|
91
91
|
3. Injects `WORKFLOWS.md` content (if exists)
|
|
92
92
|
4. Generates complete `SKILL.md` files with:
|
|
93
93
|
- Snippet table
|
package/snippets/SCHEMA.md
CHANGED
|
@@ -207,6 +207,8 @@ A script wrapped in `void (async () => {...})()` discards its return value. Neve
|
|
|
207
207
|
}
|
|
208
208
|
```
|
|
209
209
|
|
|
210
|
+
`details.element` is a CSS selector that matches only the LCP element, so `document.querySelector(details.element)` returns it: the id when it is unique, otherwise a path of tag, classes and `:nth-of-type()` from the nearest ancestor with a unique id, such as `section.intro:nth-of-type(2) > p.copy:nth-of-type(2)`. LCP-Subparts (`details.element`) and LCP-Trail (`finalElement` and each `selector`) build it the same way.
|
|
211
|
+
|
|
210
212
|
#### CLS
|
|
211
213
|
|
|
212
214
|
Returns buffered CLS immediately and keeps tracking. Always call `getCLS()` after interactions to get an updated value.
|
|
@@ -421,7 +423,16 @@ If no interactions yet, `getINP()` returns `status: "error"` with `getDataFn: "g
|
|
|
421
423
|
"serverResponse": { "value": 165, "unit": "ms" }
|
|
422
424
|
},
|
|
423
425
|
"slowestPhase": "serverResponse"
|
|
424
|
-
}
|
|
426
|
+
},
|
|
427
|
+
"issues": []
|
|
428
|
+
}
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
When the rating is not good, `issues` holds one entry that names the TTFB and the slowest phase, a `warning` for `needs-improvement` and an `error` for `poor`:
|
|
432
|
+
|
|
433
|
+
```json
|
|
434
|
+
{
|
|
435
|
+
"issues": [{ "severity": "warning", "message": "TTFB is 996 ms (needs-improvement); server response takes 993 ms of it (100%). Fix: optimize server processing, cache the HTML and check slow queries." }]
|
|
425
436
|
}
|
|
426
437
|
```
|
|
427
438
|
|
|
@@ -744,7 +755,7 @@ Issues keep `{ severity, message }`. The fix is the last sentence of the message
|
|
|
744
755
|
|
|
745
756
|
#### CSS-Media-Queries-Analysis
|
|
746
757
|
|
|
747
|
-
|
|
758
|
+
One item per distinct media query that targets viewports wider than `details.minWidth`, largest first. `details` gives the totals, `totalUnique` and `uniquenessRatio`, and `bySource` breaks the rules down by stylesheet.
|
|
748
759
|
|
|
749
760
|
```json
|
|
750
761
|
{
|
|
@@ -752,16 +763,21 @@ Issues keep `{ severity, message }`. The fix is the last sentence of the message
|
|
|
752
763
|
"status": "ok",
|
|
753
764
|
"count": 2,
|
|
754
765
|
"details": {
|
|
755
|
-
"
|
|
756
|
-
"
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
}
|
|
766
|
+
"minWidth": 768,
|
|
767
|
+
"rules": 3,
|
|
768
|
+
"totalUnique": 2,
|
|
769
|
+
"uniquenessRatio": 0.667,
|
|
770
|
+
"classes": 4,
|
|
771
|
+
"properties": 5,
|
|
772
|
+
"bytes": 160,
|
|
773
|
+
"bySource": [{ "source": "https://example.com/app.css", "inline": false, "rules": 3, "bytes": 160 }],
|
|
774
|
+
"corsBlockedCount": 0
|
|
775
|
+
},
|
|
776
|
+
"items": [
|
|
777
|
+
{ "mediaQuery": "(min-width: 1024px)", "rules": 2, "files": 1, "classes": 3, "properties": 3, "bytes": 110 },
|
|
778
|
+
{ "mediaQuery": "(min-width: 1200px)", "rules": 1, "files": 1, "classes": 1, "properties": 2, "bytes": 50 }
|
|
779
|
+
],
|
|
780
|
+
"issues": [{ "severity": "info", "message": "3 @media rules for viewports wider than 768px add 160.0 B of CSS that narrower screens download and parse without using. ..." }]
|
|
765
781
|
}
|
|
766
782
|
```
|
|
767
783
|
|
|
@@ -1089,7 +1105,7 @@ Same shape as Image-Element-Audit but for video elements.
|
|
|
1089
1105
|
|
|
1090
1106
|
#### SVG-Embedded-Bitmap-Analysis
|
|
1091
1107
|
|
|
1092
|
-
`details` counts the SVGs, also on a page with none. `spriteOpportunity` is `true` when five or more standalone inline SVGs could share one `<symbol>` sprite.
|
|
1108
|
+
`details` counts the SVGs, also on a page with none. `spriteOpportunity` is `true` when five or more standalone inline SVGs could share one `<symbol>` sprite, and it is then also an `info` issue with the counts.
|
|
1093
1109
|
|
|
1094
1110
|
```json
|
|
1095
1111
|
{
|
|
@@ -1098,7 +1114,10 @@ Same shape as Image-Element-Audit but for video elements.
|
|
|
1098
1114
|
"count": 2,
|
|
1099
1115
|
"details": { "externalSvgCount": 3, "inlineSvgTotal": 7, "svgsWithUse": 1, "standaloneInlineSvgs": 6, "spriteOpportunity": true },
|
|
1100
1116
|
"items": [{ "url": "icon.svg", "hasBitmap": true, "bitmapType": "image/png", "sizeBytes": 4500 }],
|
|
1101
|
-
"issues": [
|
|
1117
|
+
"issues": [
|
|
1118
|
+
{ "severity": "warning", "message": "2 SVG file(s) contain embedded bitmaps" },
|
|
1119
|
+
{ "severity": "info", "message": "6 standalone inline SVGs could share one <symbol> sprite referenced with <use> (1 inline SVG already uses <use>)" }
|
|
1120
|
+
]
|
|
1102
1121
|
}
|
|
1103
1122
|
```
|
|
1104
1123
|
|
|
@@ -59,3 +59,62 @@ function formatBytes(bytes) {
|
|
|
59
59
|
return (bytes / Math.pow(1024, i)).toFixed(1) + " " + units[i];
|
|
60
60
|
}
|
|
61
61
|
// @end-shared formatBytes
|
|
62
|
+
|
|
63
|
+
// @shared ownRequestUrl
|
|
64
|
+
// The URL a snippet requests when it reads a file of the page itself (fetch, a HEAD request,
|
|
65
|
+
// new Image()). The fragment never reaches the server or the HTTP cache key, so the file still
|
|
66
|
+
// comes from the cache, but it stays in the name of the Resource Timing entry of the request,
|
|
67
|
+
// which is how isOwnRequest leaves these requests out of what the page loaded.
|
|
68
|
+
function ownRequestUrl(url) {
|
|
69
|
+
try {
|
|
70
|
+
const parsed = new URL(url, location.href);
|
|
71
|
+
if (!/^https?:$/.test(parsed.protocol)) return url;
|
|
72
|
+
parsed.hash = "webperf-snippets";
|
|
73
|
+
return parsed.href;
|
|
74
|
+
} catch {
|
|
75
|
+
return url;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// @end-shared ownRequestUrl
|
|
79
|
+
|
|
80
|
+
// @shared isOwnRequest
|
|
81
|
+
// True for the Resource Timing entry of a request a snippet made with ownRequestUrl: the page
|
|
82
|
+
// did not make it, so no snippet counts it.
|
|
83
|
+
function isOwnRequest(entry) {
|
|
84
|
+
return entry.name.endsWith("#webperf-snippets");
|
|
85
|
+
}
|
|
86
|
+
// @end-shared isOwnRequest
|
|
87
|
+
|
|
88
|
+
// @shared uniqueSelector
|
|
89
|
+
// A CSS selector that matches only el in its document (or shadow root): its id when it is unique,
|
|
90
|
+
// otherwise a path of tag, classes and :nth-of-type() that goes up to the nearest ancestor with a
|
|
91
|
+
// unique id, or to the root, and stops as soon as it matches one element.
|
|
92
|
+
function uniqueSelector(el) {
|
|
93
|
+
const root = el.getRootNode();
|
|
94
|
+
const matchesOne = (selector) => {
|
|
95
|
+
try {
|
|
96
|
+
return root.querySelectorAll(selector).length === 1;
|
|
97
|
+
} catch {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
const parts = [];
|
|
102
|
+
let node = el;
|
|
103
|
+
while (node && node.nodeType === 1) {
|
|
104
|
+
if (node.id && matchesOne(`#${window.CSS.escape(node.id)}`)) {
|
|
105
|
+
parts.unshift(`#${window.CSS.escape(node.id)}`);
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
const tag = node.localName;
|
|
109
|
+
let part = tag + Array.from(node.classList).slice(0, 2).map((c) => `.${window.CSS.escape(c)}`).join("");
|
|
110
|
+
const siblings = node.parentNode ? Array.from(node.parentNode.children) : [node];
|
|
111
|
+
if (siblings.filter((s) => s.matches(part)).length > 1) {
|
|
112
|
+
part += `:nth-of-type(${siblings.filter((s) => s.localName === tag).indexOf(node) + 1})`;
|
|
113
|
+
}
|
|
114
|
+
parts.unshift(part);
|
|
115
|
+
if (matchesOne(parts.join(" > "))) break;
|
|
116
|
+
node = node.parentElement;
|
|
117
|
+
}
|
|
118
|
+
return parts.join(" > ");
|
|
119
|
+
}
|
|
120
|
+
// @end-shared uniqueSelector
|