browser-debugger-cli 0.10.0 → 0.12.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/.claude/skills/bdg/SKILL.md +268 -0
- package/README.md +148 -74
- package/dist/commands/css.d.ts +13 -0
- package/dist/commands/css.js +53 -0
- package/dist/commands/dom/audit.d.ts +14 -0
- package/dist/commands/dom/audit.js +87 -0
- package/dist/commands/dom/formInteraction.js +36 -6
- package/dist/commands/dom/helpers/keyAttributes.d.ts +3 -2
- package/dist/commands/dom/helpers/keyAttributes.js +6 -4
- package/dist/commands/dom/helpers/screenshot.d.ts +1 -0
- package/dist/commands/dom/helpers/screenshot.js +158 -38
- package/dist/commands/dom/index.js +4 -1
- package/dist/commands/dom/screenshot.js +10 -6
- package/dist/commands/dom/wait.js +5 -3
- package/dist/commands/helpJson.js +1 -1
- package/dist/commands/installSkill.d.ts +20 -0
- package/dist/commands/installSkill.js +87 -0
- package/dist/commands/optionBehaviors.js +21 -6
- package/dist/commands/page.js +7 -4
- package/dist/commands/peek.d.ts +7 -0
- package/dist/commands/peek.js +65 -23
- package/dist/commands/shared/optionTypes.d.ts +5 -1
- package/dist/commands/start.d.ts +13 -0
- package/dist/commands/start.js +19 -2
- package/dist/commands/tail.d.ts +7 -1
- package/dist/commands/tail.js +13 -62
- package/dist/commands.js +5 -0
- package/dist/daemon/session/commandRegistry.js +7 -1
- package/dist/daemon/session/plugins.js +4 -52
- package/dist/daemon.js +7986 -6848
- package/dist/errors/messages.d.ts +50 -4
- package/dist/errors/messages.js +94 -5
- package/dist/index.js +709 -190
- package/dist/ipc/client.d.ts +4 -0
- package/dist/ipc/client.js +8 -0
- package/dist/ipc/protocol/auditTypes.d.ts +129 -0
- package/dist/ipc/protocol/auditTypes.js +6 -0
- package/dist/ipc/protocol/commands.d.ts +23 -0
- package/dist/ipc/protocol/commands.js +2 -0
- package/dist/ipc/protocol/domTypes.d.ts +4 -0
- package/dist/ipc/protocol/inspectTypes.d.ts +71 -8
- package/dist/runtime/css/search.d.ts +39 -0
- package/dist/runtime/css/search.js +122 -0
- package/dist/runtime/dom/actionEffects.d.ts +4 -1
- package/dist/runtime/dom/actionEffects.js +8 -4
- package/dist/runtime/dom/audit.d.ts +19 -0
- package/dist/runtime/dom/audit.js +36 -0
- package/dist/runtime/dom/auditModel.d.ts +45 -0
- package/dist/runtime/dom/auditModel.js +215 -0
- package/dist/runtime/dom/auditScripts.d.ts +107 -0
- package/dist/runtime/dom/auditScripts.js +112 -0
- package/dist/runtime/dom/elementGeometry.d.ts +8 -2
- package/dist/runtime/dom/elementGeometry.js +24 -8
- package/dist/runtime/dom/elementInfo.d.ts +3 -2
- package/dist/runtime/dom/elementInfo.js +8 -2
- package/dist/runtime/dom/formFillHelpers/fill.js +2 -2
- package/dist/runtime/dom/inspect.d.ts +7 -0
- package/dist/runtime/dom/inspect.js +88 -23
- package/dist/runtime/dom/inspectAllStyles.d.ts +16 -4
- package/dist/runtime/dom/inspectAllStyles.js +89 -7
- package/dist/runtime/dom/inspectCascade.d.ts +19 -2
- package/dist/runtime/dom/inspectCascade.js +214 -44
- package/dist/runtime/dom/inspectCascadeModel.d.ts +8 -0
- package/dist/runtime/dom/inspectCascadeModel.js +108 -34
- package/dist/runtime/dom/inspectHints.d.ts +26 -3
- package/dist/runtime/dom/inspectHints.js +125 -9
- package/dist/runtime/dom/inspectModel.d.ts +3 -0
- package/dist/runtime/dom/inspectModel.js +30 -7
- package/dist/runtime/dom/inspectPaintModel.d.ts +48 -22
- package/dist/runtime/dom/inspectPaintModel.js +180 -68
- package/dist/runtime/dom/inspectRules.d.ts +19 -0
- package/dist/runtime/dom/inspectRules.js +21 -5
- package/dist/runtime/dom/inspectScripts.d.ts +85 -12
- package/dist/runtime/dom/inspectScripts.js +314 -28
- package/dist/runtime/dom/inspectTree.js +10 -2
- package/dist/runtime/dom/inspectWhyModel.d.ts +2 -1
- package/dist/runtime/dom/inspectWhyModel.js +52 -10
- package/dist/runtime/dom/layout.js +31 -9
- package/dist/runtime/dom/reactEventHelpers.d.ts +7 -0
- package/dist/runtime/dom/reactEventHelpers.js +27 -9
- package/dist/runtime/page/emulation.d.ts +13 -4
- package/dist/runtime/page/emulation.js +69 -4
- package/dist/runtime/page/userAgent.d.ts +17 -0
- package/dist/runtime/page/userAgent.js +57 -0
- package/dist/types.d.ts +12 -0
- package/dist/ui/formatters/audit.d.ts +19 -0
- package/dist/ui/formatters/audit.js +106 -0
- package/dist/ui/formatters/dom.d.ts +1 -1
- package/dist/ui/formatters/dom.js +6 -3
- package/dist/ui/formatters/inspect.js +42 -15
- package/dist/ui/formatters/installSkill.d.ts +11 -0
- package/dist/ui/formatters/installSkill.js +31 -0
- package/dist/ui/formatters/status.js +1 -1
- package/dist/ui/messages/commands.d.ts +44 -7
- package/dist/ui/messages/commands.js +83 -11
- package/dist/ui/messages/preview.d.ts +6 -0
- package/dist/ui/messages/preview.js +9 -1
- package/dist/utils/cssValues.js +36 -4
- package/dist/utils/decisionTrees.js +0 -5
- package/dist/utils/suggestions.d.ts +4 -2
- package/dist/utils/suggestions.js +7 -5
- package/dist/utils/taskMappings.js +1 -1
- package/package.json +4 -2
|
@@ -29,26 +29,105 @@ const FLAT_TREE_JS = `(view) => {
|
|
|
29
29
|
while (p && style(p).display === 'contents') p = flatParent(p);
|
|
30
30
|
return p;
|
|
31
31
|
};
|
|
32
|
+
const via = new WeakMap();
|
|
33
|
+
const label = (n) => n.localName + (n.id ? '#' + n.id : n.classList && n.classList.length ? '.' + n.classList[0] : '');
|
|
34
|
+
const slotText = (slot) => slot.assignedNodes({ flatten: true }).some((t) => t.nodeType === 3 && t.data.trim() !== '');
|
|
32
35
|
const children = (n) => {
|
|
33
36
|
const out = [];
|
|
34
|
-
const add = (c) => {
|
|
37
|
+
const add = (c, through) => {
|
|
35
38
|
if (skipped.test(c.localName)) return;
|
|
36
39
|
if (c.localName === 'slot') {
|
|
37
40
|
const assigned = c.assignedElements({ flatten: true });
|
|
38
|
-
(assigned.length
|
|
41
|
+
if (assigned.length === 0 && slotText(c)) out.push(c);
|
|
42
|
+
else (assigned.length ? assigned : Array.from(c.children)).forEach((a) => add(a, label(c)));
|
|
39
43
|
} else if (style(c).display === 'contents') {
|
|
40
|
-
Array.from((c.shadowRoot || c).children).forEach(add);
|
|
44
|
+
Array.from((c.shadowRoot || c).children).forEach((a) => add(a, label(c) + ' (contents)'));
|
|
41
45
|
} else {
|
|
46
|
+
if (through) via.set(c, through);
|
|
42
47
|
out.push(c);
|
|
43
48
|
}
|
|
44
49
|
};
|
|
45
|
-
Array.from((n.shadowRoot || n).children).forEach(add);
|
|
50
|
+
Array.from((n.shadowRoot || n).children).forEach((c) => add(c, null));
|
|
46
51
|
return out;
|
|
47
52
|
};
|
|
48
53
|
const rendered = (n) => n.getClientRects().length > 0 &&
|
|
49
54
|
(!n.checkVisibility || n.checkVisibility({ visibilityProperty: true }));
|
|
50
55
|
const container = (n) => children(n).some((c) => rendered(c) && !/^inline/.test(style(c).display));
|
|
51
|
-
return { style, flatParent, layoutParent, children, rendered, container };
|
|
56
|
+
return { style, skipped, flatParent, layoutParent, children, rendered, container, label, via, slotText };
|
|
57
|
+
}`;
|
|
58
|
+
/**
|
|
59
|
+
* Page-side holder of an element's text: the element whose styles draw most
|
|
60
|
+
* of the visible text in its flat tree (characters, whitespace aside), and
|
|
61
|
+
* the element whose child text nodes those are (for the rendered font).
|
|
62
|
+
* Text slotted into a shadow root is drawn with the slot's styles, and text
|
|
63
|
+
* of an element with a shadow root that is not slotted is not drawn. Null
|
|
64
|
+
* when there is no visible text.
|
|
65
|
+
*/
|
|
66
|
+
const TEXT_HOLDER_JS = `(el, tree) => {
|
|
67
|
+
const counts = new Map();
|
|
68
|
+
const parents = new Map();
|
|
69
|
+
let budget = 2000;
|
|
70
|
+
const shown = (n) => tree.style(n).display === 'contents' || tree.rendered(n);
|
|
71
|
+
const nodesOf = (n) => {
|
|
72
|
+
if (n.localName !== 'slot') return Array.from((n.shadowRoot || n).childNodes);
|
|
73
|
+
const assigned = n.assignedNodes({ flatten: true });
|
|
74
|
+
return assigned.length ? assigned : Array.from(n.childNodes);
|
|
75
|
+
};
|
|
76
|
+
const visit = (n) => {
|
|
77
|
+
if (budget-- <= 0) return;
|
|
78
|
+
for (const c of nodesOf(n)) {
|
|
79
|
+
if (budget <= 0) return;
|
|
80
|
+
if (c.nodeType === 3) {
|
|
81
|
+
const length = c.data.replace(/\\s+/g, '').length;
|
|
82
|
+
if (length === 0) continue;
|
|
83
|
+
counts.set(n, (counts.get(n) || 0) + length);
|
|
84
|
+
if (!parents.has(n)) parents.set(n, c.parentElement || n);
|
|
85
|
+
} else if (c.nodeType === 1 && !tree.skipped.test(c.localName) && shown(c)) {
|
|
86
|
+
visit(c);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
visit(el);
|
|
91
|
+
let best = null;
|
|
92
|
+
for (const [holder, length] of counts) if (!best || length > counts.get(best)) best = holder;
|
|
93
|
+
let total = 0;
|
|
94
|
+
for (const length of counts.values()) total += length;
|
|
95
|
+
return best && { style: best, font: parents.get(best), share: counts.get(best) / total };
|
|
96
|
+
}`;
|
|
97
|
+
/**
|
|
98
|
+
* Page-side: whether loaded `@font-face` (or `FontFace`) faces of the first
|
|
99
|
+
* family of an element's `font-family` cover the characters of its text
|
|
100
|
+
* (their `unicode-range`): then that web font draws the text, whatever
|
|
101
|
+
* name its file gives. A subset that lacks the text's characters (latin
|
|
102
|
+
* loaded, Cyrillic text) does not count.
|
|
103
|
+
*/
|
|
104
|
+
const FAMILY_LOADED_JS = `(n, tree, text) => {
|
|
105
|
+
const unquote = (name) => name.trim().replace(/^["']|["']$/g, '').toLowerCase();
|
|
106
|
+
const match = /^\\s*("[^"]*"|'[^']*'|[^,]*)/.exec(tree.style(n).fontFamily);
|
|
107
|
+
const first = unquote(match ? match[1] : '');
|
|
108
|
+
if (!first || !n.ownerDocument.fonts) return false;
|
|
109
|
+
const ranges = [];
|
|
110
|
+
for (const face of n.ownerDocument.fonts) {
|
|
111
|
+
if (face.status !== 'loaded' || unquote(face.family) !== first) continue;
|
|
112
|
+
for (const part of face.unicodeRange.split(',')) {
|
|
113
|
+
const [from, to] = part.trim().replace(/^U\\+/i, '').split('-');
|
|
114
|
+
const low = parseInt(from.replace(/\\?/g, '0'), 16);
|
|
115
|
+
const high = parseInt((to || from).replace(/\\?/g, 'F'), 16);
|
|
116
|
+
if (!Number.isNaN(low) && !Number.isNaN(high)) ranges.push([low, high]);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (ranges.length === 0) return false;
|
|
120
|
+
const covered = (code) => ranges.some(([low, high]) => code >= low && code <= high);
|
|
121
|
+
return Array.from(text.replace(/\\s+/g, '')).every((char) => covered(char.codePointAt(0)));
|
|
122
|
+
}`;
|
|
123
|
+
/**
|
|
124
|
+
* Page-side: whether a form control draws text of its own (a text field,
|
|
125
|
+
* a select, a button-like input); checkboxes, radios, ranges and color
|
|
126
|
+
* inputs do not. Buttons are not covered: their text is their content.
|
|
127
|
+
*/
|
|
128
|
+
const TEXT_CONTROL_JS = `(el) => {
|
|
129
|
+
if (el.localName === 'textarea' || el.localName === 'select') return true;
|
|
130
|
+
return el.localName === 'input' && !/^(checkbox|radio|range|color|hidden|image)$/.test(el.type);
|
|
52
131
|
}`;
|
|
53
132
|
/**
|
|
54
133
|
* Page-side text of an element: a form control's value or a select's chosen
|
|
@@ -100,14 +179,19 @@ const PLACEMENT_JS = `(el, tree) => {
|
|
|
100
179
|
const r = el.getBoundingClientRect();
|
|
101
180
|
const p = parent.getBoundingClientRect();
|
|
102
181
|
const ps = tree.style(parent);
|
|
103
|
-
const
|
|
182
|
+
const zoom = parent.currentCSSZoom || 1;
|
|
183
|
+
const edge = (side) => (parseFloat(ps['border-' + side + '-width']) + parseFloat(ps['padding-' + side])) * zoom;
|
|
104
184
|
const inParent = {
|
|
105
185
|
left: r.left - (p.left + edge('left')),
|
|
106
186
|
top: r.top - (p.top + edge('top')),
|
|
107
187
|
right: (p.right - edge('right')) - r.right,
|
|
108
188
|
bottom: (p.bottom - edge('bottom')) - r.bottom
|
|
109
189
|
};
|
|
110
|
-
const inFlow = (n) =>
|
|
190
|
+
const inFlow = (n) => {
|
|
191
|
+
if (n === el || !tree.rendered(n) || /^(absolute|fixed)$/.test(tree.style(n).position)) return false;
|
|
192
|
+
const box = n.getBoundingClientRect();
|
|
193
|
+
return box.width > 0 || box.height > 0;
|
|
194
|
+
};
|
|
111
195
|
const siblings = tree.children(parent);
|
|
112
196
|
const at = siblings.indexOf(el);
|
|
113
197
|
const before = siblings.slice(0, Math.max(at, 0)).reverse().find(inFlow);
|
|
@@ -135,8 +219,10 @@ const BACKGROUNDS_JS = `(el, tree, view) => {
|
|
|
135
219
|
let opacity = 1;
|
|
136
220
|
for (let n = el; n && backgrounds.length < 60; n = tree.flatParent(n)) {
|
|
137
221
|
const s = tree.style(n);
|
|
138
|
-
|
|
139
|
-
|
|
222
|
+
const own = Number(s.opacity) || 0;
|
|
223
|
+
const background = { color: s.backgroundColor, image: s.backgroundImage !== 'none' };
|
|
224
|
+
backgrounds.push(own < 1 ? Object.assign(background, { opacity: own }) : background);
|
|
225
|
+
opacity *= own;
|
|
140
226
|
}
|
|
141
227
|
const doc = el.ownerDocument;
|
|
142
228
|
const meta = doc.querySelector('meta[name="color-scheme"]');
|
|
@@ -146,6 +232,61 @@ const BACKGROUNDS_JS = `(el, tree, view) => {
|
|
|
146
232
|
const canvasDark = /dark/.test(scheme) && (!/light/.test(scheme) || prefersDark);
|
|
147
233
|
return { backgrounds: backgrounds, canvasDark: canvasDark, opacity: opacity };
|
|
148
234
|
}`;
|
|
235
|
+
/**
|
|
236
|
+
* Page-side reasons the contrast of an element's text is approximate: a
|
|
237
|
+
* blend mode or filter on it or an ancestor, and, hit-testing the middle of
|
|
238
|
+
* its first line of text (else of its first box; only in the viewport), the
|
|
239
|
+
* nearest element below it that is not an ancestor, above the first opaque
|
|
240
|
+
* background of its own chain, and
|
|
241
|
+
* paints (a canvas, video, image, background) and the nearest one on top of
|
|
242
|
+
* it that paints (an overlay).
|
|
243
|
+
*/
|
|
244
|
+
const PAINT_RISKS_JS = `(el, tree, textParent) => {
|
|
245
|
+
const risks = [];
|
|
246
|
+
const chain = [];
|
|
247
|
+
for (let n = el; n && chain.length < 60; n = tree.flatParent(n)) chain.push(n);
|
|
248
|
+
for (const n of chain) {
|
|
249
|
+
const s = tree.style(n);
|
|
250
|
+
if (s.mixBlendMode !== 'normal') risks.push('mix-blend-mode ' + s.mixBlendMode + ' on ' + tree.label(n));
|
|
251
|
+
if (s.filter !== 'none') risks.push('filter on ' + tree.label(n));
|
|
252
|
+
}
|
|
253
|
+
const textNode = Array.from(textParent.childNodes).find((c) => c.nodeType === 3 && c.data.trim() !== '');
|
|
254
|
+
const range = el.ownerDocument.createRange();
|
|
255
|
+
if (textNode) range.selectNodeContents(textNode);
|
|
256
|
+
const box = tree.style(el).display === 'contents' ? tree.layoutParent(el) : el;
|
|
257
|
+
const rect = (textNode && range.getClientRects()[0]) || (box && box.getClientRects()[0]);
|
|
258
|
+
const view = el.ownerDocument.defaultView;
|
|
259
|
+
if (!rect) return risks;
|
|
260
|
+
const x = rect.left + rect.width / 2;
|
|
261
|
+
const y = rect.top + rect.height / 2;
|
|
262
|
+
if (x < 0 || y < 0 || x >= view.innerWidth || y >= view.innerHeight) return risks;
|
|
263
|
+
const root = el.getRootNode();
|
|
264
|
+
const hits = (root.elementsFromPoint ? root : el.ownerDocument).elementsFromPoint(x, y);
|
|
265
|
+
const at = hits.findIndex((h) => chain.includes(h));
|
|
266
|
+
if (at < 0) return risks;
|
|
267
|
+
const clear = (color) => color === 'transparent' || /^rgba\\(.*,\\s*0\\)$/.test(color) || /\\/\\s*0\\)$/.test(color);
|
|
268
|
+
const opaque = (n) => {
|
|
269
|
+
const s = tree.style(n);
|
|
270
|
+
return /^rgb\\(/.test(s.backgroundColor) && s.backgroundImage === 'none' && Number(s.opacity) === 1;
|
|
271
|
+
};
|
|
272
|
+
const paints = (n) => {
|
|
273
|
+
const s = tree.style(n);
|
|
274
|
+
return /^(canvas|video|img|iframe|embed|object|svg)$/.test(n.localName) || s.backgroundImage !== 'none' || !clear(s.backgroundColor);
|
|
275
|
+
};
|
|
276
|
+
let behind = null;
|
|
277
|
+
for (const h of hits.slice(at)) {
|
|
278
|
+
if (chain.includes(h)) {
|
|
279
|
+
if (opaque(h)) break;
|
|
280
|
+
} else if (paints(h)) {
|
|
281
|
+
behind = h;
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
if (behind) risks.push(tree.label(behind) + ' behind');
|
|
286
|
+
const onTop = hits.slice(0, at).find((h) => !el.contains(h) && paints(h));
|
|
287
|
+
if (onTop) risks.push(tree.label(onTop) + ' on top');
|
|
288
|
+
return risks;
|
|
289
|
+
}`;
|
|
149
290
|
/**
|
|
150
291
|
* Page-side child tree to `depth` levels: per element its label, size,
|
|
151
292
|
* display, position relative to its parent's border box (like Figma's x/y
|
|
@@ -157,9 +298,8 @@ const BACKGROUNDS_JS = `(el, tree, view) => {
|
|
|
157
298
|
const TREE_JS = `(el, tree, textOf, depth) => {
|
|
158
299
|
let budget = ${TREE_NODE_CAP};
|
|
159
300
|
let skipped = 0;
|
|
160
|
-
const labelOf = (n) => n.localName + (n.id ? '#' + n.id : n.classList && n.classList.length ? '.' + n.classList[0] : '');
|
|
161
301
|
const walk = (kids, level, origin) => {
|
|
162
|
-
const shown = kids.filter(tree.rendered);
|
|
302
|
+
const shown = kids.filter((k) => tree.rendered(k) || k.localName === 'slot');
|
|
163
303
|
const nodes = [];
|
|
164
304
|
for (const k of shown) {
|
|
165
305
|
if (budget <= 0) { skipped++; continue; }
|
|
@@ -170,7 +310,13 @@ const TREE_JS = `(el, tree, textOf, depth) => {
|
|
|
170
310
|
};
|
|
171
311
|
const nodeOf = (n, level, origin) => {
|
|
172
312
|
const r = n.getBoundingClientRect();
|
|
173
|
-
const node = { label:
|
|
313
|
+
const node = { label: tree.label(n), x: r.left - origin.left, y: r.top - origin.top, w: r.width, h: r.height, display: tree.style(n).display };
|
|
314
|
+
if (tree.via.has(n)) node.via = tree.via.get(n);
|
|
315
|
+
if (n.parentNode && n.parentNode.nodeType === 11 && n.parentNode.host) node.shadow = true;
|
|
316
|
+
if (n.localName === 'slot') {
|
|
317
|
+
node.text = n.assignedNodes({ flatten: true }).map((t) => t.textContent).join(' ').replace(/\\s+/g, ' ').trim().slice(0, 60);
|
|
318
|
+
return node;
|
|
319
|
+
}
|
|
174
320
|
const kids = tree.children(n);
|
|
175
321
|
if (!tree.container(n)) node.text = textOf(n).slice(0, 60);
|
|
176
322
|
if (level >= depth) {
|
|
@@ -186,12 +332,49 @@ const TREE_JS = `(el, tree, textOf, depth) => {
|
|
|
186
332
|
const top = walk(tree.children(el), 1, el.getBoundingClientRect());
|
|
187
333
|
return { tree: top.nodes, hiddenChildren: top.hidden, treeSkipped: skipped };
|
|
188
334
|
}`;
|
|
335
|
+
/**
|
|
336
|
+
* Page-side: rules for the element that set a property but sit under a
|
|
337
|
+
* `@media` or `@supports` condition that does not apply now (at most 5),
|
|
338
|
+
* from same-origin, `@import`ed, constructed and shadow-root stylesheets.
|
|
339
|
+
*/
|
|
340
|
+
const INACTIVE_RULES_JS = `(el, names) => {
|
|
341
|
+
const view = el.ownerDocument.defaultView;
|
|
342
|
+
const found = [];
|
|
343
|
+
let budget = 20000;
|
|
344
|
+
const unmet = (rule) => {
|
|
345
|
+
if (rule.media && rule.media.mediaText && !view.matchMedia(rule.media.mediaText).matches) return '@media ' + rule.media.mediaText;
|
|
346
|
+
if (typeof CSSSupportsRule !== 'undefined' && rule instanceof CSSSupportsRule && !CSS.supports(rule.conditionText)) return '@supports ' + rule.conditionText;
|
|
347
|
+
return null;
|
|
348
|
+
};
|
|
349
|
+
const sets = (rule) => names.map((name) => rule.style.getPropertyValue(name).trim()).find((value) => value !== '');
|
|
350
|
+
const matches = (selector) => {
|
|
351
|
+
try { return el.matches(selector); } catch (e) { return false; }
|
|
352
|
+
};
|
|
353
|
+
const visit = (rules, condition) => {
|
|
354
|
+
for (const rule of Array.from(rules)) {
|
|
355
|
+
if (budget-- <= 0 || found.length >= 5) return;
|
|
356
|
+
if (typeof CSSImportRule !== 'undefined' && rule instanceof CSSImportRule) {
|
|
357
|
+
try { if (rule.styleSheet) visit(rule.styleSheet.cssRules, condition); } catch (e) { continue; }
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
const value = condition && rule.style && rule.selectorText ? sets(rule) : undefined;
|
|
361
|
+
if (value !== undefined && matches(rule.selectorText)) found.push({ selector: rule.selectorText, condition: condition, value: value });
|
|
362
|
+
if (rule.cssRules) visit(rule.cssRules, condition || unmet(rule));
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
for (const scope of new Set([el.getRootNode(), el.ownerDocument])) {
|
|
366
|
+
for (const sheet of [...Array.from(scope.styleSheets || []), ...Array.from(scope.adoptedStyleSheets || [])]) {
|
|
367
|
+
try { visit(sheet.cssRules, null); } catch (e) { continue; }
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return found;
|
|
371
|
+
}`;
|
|
189
372
|
/**
|
|
190
373
|
* Page-side function run on the element (`this`), see {@link RawInspect}.
|
|
191
|
-
* Arguments: tree depth,
|
|
192
|
-
* (or null).
|
|
374
|
+
* Arguments: tree depth, the property names asked for with `--props` (or
|
|
375
|
+
* null) and the `--why` property (or null).
|
|
193
376
|
*/
|
|
194
|
-
export const INSPECT_PAGE_JS = `function (depth, props) {
|
|
377
|
+
export const INSPECT_PAGE_JS = `function (depth, props, why) {
|
|
195
378
|
const el = this;
|
|
196
379
|
const view = el.ownerDocument.defaultView;
|
|
197
380
|
const tree = (${FLAT_TREE_JS})(view);
|
|
@@ -199,6 +382,11 @@ export const INSPECT_PAGE_JS = `function (depth, props) {
|
|
|
199
382
|
const describe = ${ELEMENT_DESCRIPTION_JS};
|
|
200
383
|
const s = tree.style(el);
|
|
201
384
|
const formControl = /^(input|textarea|select|button)$/.test(el.localName);
|
|
385
|
+
const textControl = (${TEXT_CONTROL_JS})(el);
|
|
386
|
+
const holder = textControl ? null : (${TEXT_HOLDER_JS})(el, tree);
|
|
387
|
+
const textChildren = tree.children(el).filter((c) => tree.rendered(c) && (c.textContent || '').trim() !== '').length;
|
|
388
|
+
const textual = textControl || Boolean(holder && (holder.style === el || (holder.share >= 0.9 && textChildren <= 1) || !tree.container(el)));
|
|
389
|
+
const textFrom = holder && holder.style !== el ? holder.style : el;
|
|
202
390
|
const content = textOf(el);
|
|
203
391
|
const parent = tree.layoutParent(el);
|
|
204
392
|
const typedMap = el.computedStyleMap ? el.computedStyleMap() : null;
|
|
@@ -209,15 +397,26 @@ export const INSPECT_PAGE_JS = `function (depth, props) {
|
|
|
209
397
|
classes: el.classList ? Array.from(el.classList) : [],
|
|
210
398
|
context: (${ELEMENT_CONTEXT_JS})(el),
|
|
211
399
|
content: content,
|
|
212
|
-
textual:
|
|
400
|
+
textual: textual,
|
|
213
401
|
formControl: formControl,
|
|
214
|
-
|
|
402
|
+
rendered: textual && textFrom !== el
|
|
403
|
+
? tree.style(textFrom).visibility !== 'hidden' && tree.rendered(tree.style(textFrom).display === 'contents' ? tree.layoutParent(textFrom) : textFrom)
|
|
404
|
+
: tree.rendered(el),
|
|
405
|
+
hasText: textControl || holder !== null,
|
|
406
|
+
replaced: el instanceof SVGElement || /^(img|video|audio|canvas|iframe|embed|object|input|textarea|select|button|meter|progress)$/.test(el.localName),
|
|
215
407
|
svg: el.namespaceURI === 'http://www.w3.org/2000/svg',
|
|
216
|
-
ownText: Array.from(el.childNodes).some((n) => n.nodeType === 3 && n.data.trim() !== ''),
|
|
217
408
|
typed: { width: typed('width'), height: typed('height') },
|
|
218
409
|
parent: parent ? describe(parent) : undefined,
|
|
219
|
-
animating: el.getAnimations ? [...new Set(el.getAnimations().filter((a) => a.playState === 'running').map((a) => a.transitionProperty || a.animationName || 'animation'))] : []
|
|
410
|
+
animating: el.getAnimations ? [...new Set(el.getAnimations().filter((a) => a.playState === 'running' && (!a.timeline || a.timeline === el.ownerDocument.timeline)).map((a) => a.transitionProperty || a.animationName || 'animation'))] : []
|
|
220
411
|
};
|
|
412
|
+
if (textFrom !== el) result.textHolder = tree.label(textFrom);
|
|
413
|
+
const clipped = (n) => {
|
|
414
|
+
const cs = tree.style(n);
|
|
415
|
+
if (cs.overflowX !== 'visible' && n.scrollWidth > n.clientWidth + 1) return true;
|
|
416
|
+
return cs.webkitLineClamp !== 'none' && cs.overflowY !== 'visible' && n.scrollHeight > n.clientHeight + 1;
|
|
417
|
+
};
|
|
418
|
+
if (textual && !textControl && (clipped(el) || (textFrom !== el && clipped(textFrom)))) result.truncated = true;
|
|
419
|
+
if ((${FAMILY_LOADED_JS})(textFrom, tree, textOf(textFrom))) result.familyLoaded = true;
|
|
221
420
|
if (/^(input|textarea)$/.test(el.localName) && el.placeholder && !el.value) {
|
|
222
421
|
result.placeholder = el.placeholder.replace(/\\s+/g, ' ').trim();
|
|
223
422
|
const placeholderStyle = tree.style(el, '::placeholder');
|
|
@@ -236,28 +435,115 @@ export const INSPECT_PAGE_JS = `function (depth, props) {
|
|
|
236
435
|
return sides.map((side) => side.slice(name.length + 1) + ' ' + s.getPropertyValue(side).trim()).join(' / ');
|
|
237
436
|
};
|
|
238
437
|
result.props = Object.fromEntries(props.filter(known).map((name) => [name, valueOf(name)]));
|
|
438
|
+
result.rootFontSize = parseFloat(tree.style(el.ownerDocument.documentElement).fontSize) || 16;
|
|
239
439
|
result.unknownProps = props.filter((name) => !known(name));
|
|
240
440
|
}
|
|
441
|
+
if (why && !why.startsWith('--')) {
|
|
442
|
+
if (!CSS.supports(why, 'inherit')) result.unknownWhy = true;
|
|
443
|
+
else {
|
|
444
|
+
const probe = el.ownerDocument.createElement('div').style;
|
|
445
|
+
probe.setProperty(why, 'inherit');
|
|
446
|
+
if (probe.length > 1) {
|
|
447
|
+
result.whyLonghands = Array.from(probe);
|
|
448
|
+
result.whyComputed = s.getPropertyValue(why).trim();
|
|
449
|
+
}
|
|
450
|
+
const inactive = (${INACTIVE_RULES_JS})(el, [why].concat(result.whyLonghands || []));
|
|
451
|
+
if (inactive.length > 0) result.whyInactive = inactive;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
241
454
|
return Object.assign(result,
|
|
242
455
|
(${PLACEMENT_JS})(el, tree),
|
|
243
|
-
(${BACKGROUNDS_JS})(el, tree, view),
|
|
456
|
+
(${BACKGROUNDS_JS})(textual ? textFrom : el, tree, view),
|
|
457
|
+
textual ? { paintRisks: (${PAINT_RISKS_JS})(textFrom, tree, holder ? holder.font : el) } : {},
|
|
244
458
|
(${TREE_JS})(el, tree, textOf, depth));
|
|
245
459
|
}`;
|
|
246
460
|
/**
|
|
247
461
|
* Page-side function run on the element (`this`) that returns another
|
|
248
462
|
* element whose styles CDP reads: `"parent"`, the parent that lays it out
|
|
249
|
-
* (`display: contents` skipped, through shadow roots and slots),
|
|
250
|
-
* `"textHolder"`,
|
|
251
|
-
* (
|
|
252
|
-
*
|
|
463
|
+
* (`display: contents` skipped, through shadow roots and slots),
|
|
464
|
+
* `"textHolder"`, the element whose styles draw most of its text
|
|
465
|
+
* ({@link TEXT_HOLDER_JS}; the element itself when that is its own text), or
|
|
466
|
+
* `"fontHolder"`, the parent of that text's nodes (for the rendered font: a
|
|
467
|
+
* slot's text nodes belong to the host's light DOM). Null when there is none.
|
|
253
468
|
*/
|
|
254
469
|
export const RELATED_NODE_JS = `function (which) {
|
|
255
470
|
const tree = (${FLAT_TREE_JS})(this.ownerDocument.defaultView);
|
|
256
471
|
if (which === 'parent') return tree.layoutParent(this);
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
472
|
+
const holder = (${TEXT_HOLDER_JS})(this, tree);
|
|
473
|
+
if (!holder) return null;
|
|
474
|
+
return which === 'textHolder' ? holder.style : holder.font;
|
|
475
|
+
}`;
|
|
476
|
+
/**
|
|
477
|
+
* Page-side function run on the element (`this`): for each custom property
|
|
478
|
+
* name, a rule in the page's stylesheets (same-origin, `@import`ed,
|
|
479
|
+
* constructed and the element's shadow root's) that sets it, keyframes
|
|
480
|
+
* included: the first one that matches the element or an ancestor now
|
|
481
|
+
* (across shadow roots, so `:root` matches inside a component), else the
|
|
482
|
+
* first that would match without its state (`.btn:hover` for this `.btn`),
|
|
483
|
+
* else the first one, with the `@media`/`@supports` condition it is under
|
|
484
|
+
* when that does not apply. At most 20000 rules are read; cross-origin sheets are
|
|
485
|
+
* skipped.
|
|
486
|
+
*/
|
|
487
|
+
export const VARIABLE_SETTERS_JS = `function (names) {
|
|
488
|
+
const el = this;
|
|
489
|
+
const view = el.ownerDocument.defaultView;
|
|
490
|
+
const matching = {};
|
|
491
|
+
const related = {};
|
|
492
|
+
const other = {};
|
|
493
|
+
let budget = 20000;
|
|
494
|
+
const closestAcross = (selector) => {
|
|
495
|
+
for (let n = el; n; n = n.getRootNode().host || null) {
|
|
496
|
+
if (n.closest(selector)) return true;
|
|
497
|
+
}
|
|
498
|
+
return false;
|
|
499
|
+
};
|
|
500
|
+
const matches = (selector) => {
|
|
501
|
+
try { return closestAcross(selector); } catch (e) { return null; }
|
|
502
|
+
};
|
|
503
|
+
const stateless = (selector) => selector.replace(/:(focus-visible|focus-within|focus|hover|active|visited|checked|target|open)(?![\\w-])/g, '');
|
|
504
|
+
const applies = (rule) => {
|
|
505
|
+
if (rule.media && rule.media.mediaText && !view.matchMedia(rule.media.mediaText).matches) return '@media ' + rule.media.mediaText;
|
|
506
|
+
if (typeof CSSSupportsRule !== 'undefined' && rule instanceof CSSSupportsRule && !CSS.supports(rule.conditionText)) return '@supports ' + rule.conditionText;
|
|
507
|
+
return null;
|
|
508
|
+
};
|
|
509
|
+
const record = (name, rule, keyframes, condition) => {
|
|
510
|
+
const selector = keyframes ? '@keyframes ' + keyframes : rule.selectorText || '';
|
|
511
|
+
const match = keyframes || condition ? false : matches(selector);
|
|
512
|
+
const setter = Object.assign(
|
|
513
|
+
{ selector: selector, value: rule.style.getPropertyValue(name).trim(), matches: match },
|
|
514
|
+
keyframes ? { keyframes: keyframes } : {},
|
|
515
|
+
condition ? { condition: condition } : {}
|
|
516
|
+
);
|
|
517
|
+
if (match && !matching[name]) matching[name] = setter;
|
|
518
|
+
else if (!match && !keyframes && !related[name] && matches(stateless(selector))) related[name] = setter;
|
|
519
|
+
else if (!match && !other[name]) other[name] = setter;
|
|
520
|
+
};
|
|
521
|
+
const visit = (rules, keyframes, condition) => {
|
|
522
|
+
for (const rule of Array.from(rules)) {
|
|
523
|
+
if (budget-- <= 0 || names.every((name) => matching[name])) return;
|
|
524
|
+
if (typeof CSSImportRule !== 'undefined' && rule instanceof CSSImportRule) {
|
|
525
|
+
try { if (rule.styleSheet) visit(rule.styleSheet.cssRules, keyframes, condition); } catch (e) { continue; }
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
if (typeof CSSKeyframesRule !== 'undefined' && rule instanceof CSSKeyframesRule) {
|
|
529
|
+
visit(rule.cssRules, rule.name, condition);
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
if (rule.style) {
|
|
533
|
+
for (const name of names) {
|
|
534
|
+
if (!matching[name] && Array.from(rule.style).includes(name)) record(name, rule, keyframes, condition);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
if (rule.cssRules) visit(rule.cssRules, keyframes, condition || applies(rule));
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
const sheets = [];
|
|
541
|
+
for (const scope of new Set([el.getRootNode(), el.ownerDocument])) {
|
|
542
|
+
sheets.push(...Array.from(scope.styleSheets || []), ...Array.from(scope.adoptedStyleSheets || []));
|
|
543
|
+
}
|
|
544
|
+
for (const sheet of sheets) {
|
|
545
|
+
try { visit(sheet.cssRules, null, null); } catch (e) { continue; }
|
|
260
546
|
}
|
|
261
|
-
return
|
|
547
|
+
return Object.assign({}, other, related, matching);
|
|
262
548
|
}`;
|
|
263
549
|
//# sourceMappingURL=inspectScripts.js.map
|
|
@@ -40,6 +40,9 @@ function toNode(raw) {
|
|
|
40
40
|
h: Math.round(raw.h),
|
|
41
41
|
...(layout && { layout }),
|
|
42
42
|
...(raw.text && { text: rowText(raw.text) }),
|
|
43
|
+
...(raw.display === 'contents' && { contents: true }),
|
|
44
|
+
...(raw.via && { via: raw.via }),
|
|
45
|
+
...(raw.shadow && { shadow: true }),
|
|
43
46
|
...(children && children.length > 0 && { children }),
|
|
44
47
|
...(raw.childCount && { childCount: raw.childCount }),
|
|
45
48
|
...(raw.hidden && { hiddenChildren: raw.hidden }),
|
|
@@ -50,10 +53,15 @@ function toNode(raw) {
|
|
|
50
53
|
*
|
|
51
54
|
* @param a - Row
|
|
52
55
|
* @param b - Row
|
|
53
|
-
* @returns True for the same label, size and
|
|
56
|
+
* @returns True for the same label, size, layout and path (slot, shadow root)
|
|
54
57
|
*/
|
|
55
58
|
function identical(a, b) {
|
|
56
|
-
return a.element === b.element &&
|
|
59
|
+
return (a.element === b.element &&
|
|
60
|
+
a.w === b.w &&
|
|
61
|
+
a.h === b.h &&
|
|
62
|
+
a.layout === b.layout &&
|
|
63
|
+
a.via === b.via &&
|
|
64
|
+
a.shadow === b.shadow);
|
|
57
65
|
}
|
|
58
66
|
/**
|
|
59
67
|
* Group runs of identical siblings into one row with a count. A group keeps
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import type { InspectWhy } from '../../ipc/protocol/inspectTypes.js';
|
|
9
9
|
import { type Resolution } from './inspectCascade.js';
|
|
10
10
|
import type { CascadeInput, PropertyGroup } from './inspectCascadeModel.js';
|
|
11
|
+
import type { CssHint } from './inspectHints.js';
|
|
11
12
|
/**
|
|
12
13
|
* `--why` for a property.
|
|
13
14
|
*
|
|
@@ -16,5 +17,5 @@ import type { CascadeInput, PropertyGroup } from './inspectCascadeModel.js';
|
|
|
16
17
|
* @param input - Matched rules, computed style and the label function
|
|
17
18
|
* @returns One entry, or one per longhand when they differ
|
|
18
19
|
*/
|
|
19
|
-
export declare function buildWhy(group: PropertyGroup, cascade: Map<string, Resolution>, input: CascadeInput): InspectWhy[];
|
|
20
|
+
export declare function buildWhy(group: PropertyGroup, cascade: Map<string, Resolution>, input: CascadeInput, hints?: readonly CssHint[]): InspectWhy[];
|
|
20
21
|
//# sourceMappingURL=inspectWhyModel.d.ts.map
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* declaration sets all its longhands (listing every declaration it beats on
|
|
6
6
|
* any of them), else one per longhand.
|
|
7
7
|
*/
|
|
8
|
-
import { normalizeProperty } from './inspectAllStyles.js';
|
|
9
|
-
import { resolveCascade } from './inspectCascade.js';
|
|
8
|
+
import { collapsedValue, normalizeProperty } from './inspectAllStyles.js';
|
|
9
|
+
import { isInherited, resolveCascade, ruleField, } from './inspectCascade.js';
|
|
10
10
|
import { substituteVariables, unsetVariables, usedVariables, } from './inspectVariables.js';
|
|
11
11
|
/**
|
|
12
12
|
* `--why` for a property.
|
|
@@ -16,8 +16,8 @@ import { substituteVariables, unsetVariables, usedVariables, } from './inspectVa
|
|
|
16
16
|
* @param input - Matched rules, computed style and the label function
|
|
17
17
|
* @returns One entry, or one per longhand when they differ
|
|
18
18
|
*/
|
|
19
|
-
export function buildWhy(group, cascade, input) {
|
|
20
|
-
const whys = group.longhands.map((longhand) => whyOf(longhand, cascade.get(longhand), input));
|
|
19
|
+
export function buildWhy(group, cascade, input, hints = []) {
|
|
20
|
+
const whys = group.longhands.map((longhand) => whyOf(longhand, cascade.get(longhand), input, hints));
|
|
21
21
|
const [first] = whys;
|
|
22
22
|
if (!first || group.shorthand === undefined)
|
|
23
23
|
return whys;
|
|
@@ -25,9 +25,13 @@ export function buildWhy(group, cascade, input) {
|
|
|
25
25
|
if (!whys.every((why) => JSON.stringify(why.chain[0]) === winner))
|
|
26
26
|
return whys;
|
|
27
27
|
const values = whys.map((why) => why.computed);
|
|
28
|
-
const
|
|
28
|
+
const joined = values.every((value) => value === first.computed)
|
|
29
29
|
? first.computed
|
|
30
30
|
: values.join(' ');
|
|
31
|
+
const fromPage = input.whyComputed
|
|
32
|
+
? normalizeProperty(group.shorthand, input.whyComputed)
|
|
33
|
+
: undefined;
|
|
34
|
+
const computed = collapsedValue(group.shorthand, input.style) ?? fromPage ?? joined;
|
|
31
35
|
const beaten = whys.flatMap((why) => why.chain.slice(1));
|
|
32
36
|
const overridden = [...new Map(beaten.map((entry) => [JSON.stringify(entry), entry])).values()];
|
|
33
37
|
const chain = [...first.chain.slice(0, 1), ...overridden];
|
|
@@ -39,14 +43,14 @@ export function buildWhy(group, cascade, input) {
|
|
|
39
43
|
* @param property - Longhand
|
|
40
44
|
* @param resolution - Its resolution
|
|
41
45
|
* @param input - Matched rules, computed style and the label function
|
|
46
|
+
* @param hints - Hints, for a winner that has no effect
|
|
42
47
|
* @returns Why
|
|
43
48
|
*/
|
|
44
|
-
function whyOf(property, resolution, input) {
|
|
49
|
+
function whyOf(property, resolution, input, hints) {
|
|
45
50
|
const winner = resolution?.winner;
|
|
51
|
+
const status = winner?.ancestor !== undefined ? 'inherited' : 'applied';
|
|
46
52
|
const chain = [
|
|
47
|
-
...(winner
|
|
48
|
-
? [whyEntry(winner, winner.ancestor !== undefined ? 'inherited' : 'applied', input)]
|
|
49
|
-
: []),
|
|
53
|
+
...(winner ? [withNote(whyEntry(winner, status, input), winner, property, hints)] : []),
|
|
50
54
|
...(resolution?.overridden ?? []).map((d) => whyEntry(d, 'overridden', input)),
|
|
51
55
|
];
|
|
52
56
|
const variables = winner ? variableSources(winner, input) : [];
|
|
@@ -68,7 +72,10 @@ function whyOf(property, resolution, input) {
|
|
|
68
72
|
* @returns Entry
|
|
69
73
|
*/
|
|
70
74
|
function whyEntry(declaration, status, input) {
|
|
71
|
-
const
|
|
75
|
+
const written = declaration.via ? (declaration.written ?? declaration.value) : declaration.value;
|
|
76
|
+
const value = written === ''
|
|
77
|
+
? `${input.style[declaration.property] ?? ''} (set by the browser)`.trim()
|
|
78
|
+
: written;
|
|
72
79
|
const unset = value.includes('var(') ? unsetVariables(value, input.style) : [];
|
|
73
80
|
const resolved = value.includes('var(') ? resolvedValue(declaration, value, input) : value;
|
|
74
81
|
const { specificity, layer, condition } = declaration.source;
|
|
@@ -77,6 +84,7 @@ function whyEntry(declaration, status, input) {
|
|
|
77
84
|
...(declaration.via && { via: declaration.via }),
|
|
78
85
|
...(unset.length > 0 ? { unset } : resolved !== value && { resolved }),
|
|
79
86
|
source: input.label(declaration),
|
|
87
|
+
...ruleField(declaration),
|
|
80
88
|
...(specificity && { specificity }),
|
|
81
89
|
status,
|
|
82
90
|
...(declaration.important && { important: true }),
|
|
@@ -84,6 +92,40 @@ function whyEntry(declaration, status, input) {
|
|
|
84
92
|
...(condition && { condition }),
|
|
85
93
|
};
|
|
86
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* The winner's entry with why it changes nothing: the hint that says it has
|
|
97
|
+
* no effect, or what applies instead of an invalid `var()`.
|
|
98
|
+
*
|
|
99
|
+
* @param entry - Winner's entry
|
|
100
|
+
* @param winner - Winning declaration
|
|
101
|
+
* @param property - Longhand
|
|
102
|
+
* @param hints - Hints
|
|
103
|
+
* @returns Entry, with `note` when there is one
|
|
104
|
+
*/
|
|
105
|
+
function withNote(entry, winner, property, hints) {
|
|
106
|
+
if (entry.unset) {
|
|
107
|
+
const fallback = isInherited(property) ? 'the inherited value' : 'the initial value';
|
|
108
|
+
return { ...entry, note: `falls back to ${fallback}` };
|
|
109
|
+
}
|
|
110
|
+
const inactive = hints.find((hint) => hint.kind === 'inactive' &&
|
|
111
|
+
sameDeclaration(hint.declaration, winner) &&
|
|
112
|
+
(hint.only?.includes(property) ?? true));
|
|
113
|
+
return inactive ? { ...entry, note: `no effect: ${inactive.reason}` } : entry;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Whether two longhand declarations come from the same declaration as written.
|
|
117
|
+
*
|
|
118
|
+
* @param a - Declaration
|
|
119
|
+
* @param b - Declaration
|
|
120
|
+
* @returns True for the same place and property
|
|
121
|
+
*/
|
|
122
|
+
function sameDeclaration(a, b) {
|
|
123
|
+
return (a.source.kind === b.source.kind &&
|
|
124
|
+
a.source.styleSheetId === b.source.styleSheetId &&
|
|
125
|
+
a.source.line === b.source.line &&
|
|
126
|
+
a.source.column === b.source.column &&
|
|
127
|
+
(a.via ?? a.property) === (b.via ?? b.property));
|
|
128
|
+
}
|
|
87
129
|
/** How many levels of custom properties set from others are followed */
|
|
88
130
|
const MAX_VARIABLE_DEPTH = 5;
|
|
89
131
|
/**
|